JS-only crypto (only primitives so far)
This commit is contained in:
parent
d42b9e3df8
commit
c3ca556aff
@ -63,6 +63,7 @@
|
||||
"@types/chrome": "^0.0.91",
|
||||
"@types/urijs": "^1.19.3",
|
||||
"axios": "^0.19.0",
|
||||
"big-integer": "^1.6.48",
|
||||
"idb-bridge": "^0.0.14",
|
||||
"qrcode-generator": "^1.4.3",
|
||||
"source-map-support": "^0.5.12",
|
||||
|
@ -45,7 +45,7 @@ import * as native from "./emscInterface";
|
||||
import { AmountJson } from "../amounts";
|
||||
import * as Amounts from "../amounts";
|
||||
import * as timer from "../timer";
|
||||
import { getRandomBytes, encodeCrock } from "./nativeCrypto";
|
||||
import { getRandomBytes, encodeCrock } from "./talerCrypto";
|
||||
|
||||
export class CryptoImplementation {
|
||||
static enableTracing: boolean = false;
|
||||
|
@ -20,8 +20,7 @@ import test from "ava";
|
||||
import { NodeEmscriptenLoader } from "./nodeEmscriptenLoader";
|
||||
import * as native from "./emscInterface";
|
||||
|
||||
import nacl = require("./nacl-fast");
|
||||
import { encodeCrock, decodeCrock } from "./nativeCrypto";
|
||||
import { encodeCrock, decodeCrock } from "./talerCrypto";
|
||||
import { timestampCheck } from "../helpers";
|
||||
|
||||
|
||||
@ -37,12 +36,7 @@ test("string hashing", async (t) => {
|
||||
|
||||
const te = new TextEncoder();
|
||||
|
||||
const x2 = te.encode("hello taler\0")
|
||||
|
||||
const hc2 = encodeCrock(nacl.hash(x2));
|
||||
|
||||
console.log(`# hc2 ${hc}`);
|
||||
t.true(h === hc2);
|
||||
const x2 = te.encode("hello taler\0");
|
||||
|
||||
t.pass();
|
||||
});
|
||||
@ -68,29 +62,8 @@ test("signing", async (t) => {
|
||||
console.timeEnd("a");
|
||||
t.true(native.eddsaVerify(native.SignaturePurpose.TEST, purpose, sig, pub));
|
||||
|
||||
console.log("priv size", decodeCrock(privCrock).byteLength);
|
||||
|
||||
const pair = nacl.sign_keyPair_fromSeed(new Uint8Array(decodeCrock(privCrock)));
|
||||
|
||||
console.log("emsc priv", privCrock);
|
||||
console.log("emsc pub", pubCrock);
|
||||
|
||||
console.log("nacl priv", encodeCrock(pair.secretKey));
|
||||
console.log("nacl pub", encodeCrock(pair.publicKey));
|
||||
|
||||
const d2 = new Uint8Array(decodeCrock(purposeDataCrock));
|
||||
const d3 = nacl.hash(d2);
|
||||
|
||||
console.time("b");
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
const s2 = nacl.sign_detached(d3, pair.secretKey);
|
||||
}
|
||||
console.timeEnd("b");
|
||||
|
||||
const s2 = nacl.sign_detached(d3, pair.secretKey);
|
||||
|
||||
console.log("sig1:", sig.toCrock());
|
||||
console.log("sig2:", encodeCrock(s2));
|
||||
|
||||
t.pass();
|
||||
});
|
||||
|
@ -17,8 +17,6 @@
|
||||
import nacl = require("./nacl-fast");
|
||||
import { sha256 } from "./sha256";
|
||||
|
||||
let createHmac: any;
|
||||
|
||||
export function sha512(data: Uint8Array): Uint8Array {
|
||||
return nacl.hash(data);
|
||||
}
|
||||
@ -32,7 +30,6 @@ export function hmac(
|
||||
if (key.byteLength > blockSize) {
|
||||
key = digest(key);
|
||||
}
|
||||
console.log("message", message);
|
||||
if (key.byteLength < blockSize) {
|
||||
const k = key;
|
||||
key = new Uint8Array(blockSize);
|
||||
@ -62,39 +59,34 @@ export function hmacSha256(key: Uint8Array, message: Uint8Array) {
|
||||
return hmac(sha256, 64, key, message);
|
||||
}
|
||||
|
||||
/*
|
||||
function expand(prfAlgo: string, prk: Uint8Array, length: number, info: Uint8Array) {
|
||||
let hashLength;
|
||||
if (prfAlgo == "sha512") {
|
||||
hashLength = 64;
|
||||
} else if (prfAlgo == "sha256") {
|
||||
hashLength = 32;
|
||||
} else {
|
||||
throw Error("unsupported hash");
|
||||
}
|
||||
info = info || Buffer.alloc(0);
|
||||
var N = Math.ceil(length / hashLength);
|
||||
var memo: Buffer[] = [];
|
||||
|
||||
for (var i = 0; i < N; i++) {
|
||||
memo[i] = createHmac(prfAlgo, prk)
|
||||
.update(memo[i - 1] || Buffer.alloc(0))
|
||||
.update(info)
|
||||
.update(Buffer.alloc(1, i + 1))
|
||||
.digest();
|
||||
}
|
||||
return Buffer.concat(memo, length);
|
||||
}
|
||||
*/
|
||||
|
||||
export function kdf(ikm: Uint8Array, salt: Uint8Array, info: Uint8Array) {
|
||||
export function kdf(
|
||||
outputLength: number,
|
||||
ikm: Uint8Array,
|
||||
salt: Uint8Array,
|
||||
info: Uint8Array,
|
||||
): Uint8Array {
|
||||
// extract
|
||||
const prk = hmacSha512(salt, ikm);
|
||||
|
||||
// expand
|
||||
|
||||
var N = Math.ceil(length / 256);
|
||||
|
||||
//return expand(prfAlgo, prk, length, info);
|
||||
return prk;
|
||||
const N = Math.ceil(outputLength / 32);
|
||||
const output = new Uint8Array(N * 32);
|
||||
for (let i = 0; i < N; i++) {
|
||||
let buf;
|
||||
if (i == 0) {
|
||||
buf = new Uint8Array(info.byteLength + 1);
|
||||
buf.set(info, 0);
|
||||
} else {
|
||||
buf = new Uint8Array(info.byteLength + 1 + 32);
|
||||
for (let j = 0; j < 32; j++) {
|
||||
buf[j] = output[(i - 1) * 32 + j];
|
||||
}
|
||||
buf.set(info, 32);
|
||||
}
|
||||
buf[buf.length - 1] = i + 1;
|
||||
const chunk = hmacSha256(prk, buf);
|
||||
output.set(chunk, i * 32);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,85 +0,0 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2019 GNUnet e.V.
|
||||
|
||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Imports
|
||||
*/
|
||||
import test from "ava";
|
||||
import { encodeCrock, decodeCrock } from "./nativeCrypto";
|
||||
import { hmacSha512, sha512 } from "./kdf";
|
||||
import nacl = require("./nacl-fast");
|
||||
|
||||
function hexToBytes(hex: string) {
|
||||
for (var bytes = [], c = 0; c < hex.length; c += 2)
|
||||
bytes.push(parseInt(hex.substr(c, 2), 16));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function bytesToHex(bytes: Uint8Array): string {
|
||||
for (var hex = [], i = 0; i < bytes.length; i++) {
|
||||
var current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];
|
||||
hex.push((current >>> 4).toString(16));
|
||||
hex.push((current & 0xf).toString(16));
|
||||
}
|
||||
return hex.join("");
|
||||
}
|
||||
|
||||
test("encoding", t => {
|
||||
const utf8decoder = new TextDecoder("utf-8");
|
||||
const utf8encoder = new TextEncoder();
|
||||
const s = "Hello, World";
|
||||
const encStr = encodeCrock(utf8encoder.encode(s));
|
||||
const outBuf = decodeCrock(encStr);
|
||||
const sOut = utf8decoder.decode(outBuf);
|
||||
t.deepEqual(s, sOut);
|
||||
});
|
||||
|
||||
test("taler-exchange-tvg hash code", t => {
|
||||
const input = "91JPRV3F5GG4EKJN41A62V35E8";
|
||||
const output =
|
||||
"CW96WR74JS8T53EC8GKSGD49QKH4ZNFTZXDAWMMV5GJ1E4BM6B8GPN5NVHDJ8ZVXNCW7Q4WBYCV61HCA3PZC2YJD850DT29RHHN7ESR";
|
||||
|
||||
const myOutput = encodeCrock(sha512(decodeCrock(input)));
|
||||
|
||||
t.deepEqual(myOutput, output);
|
||||
});
|
||||
|
||||
test("taler-exchange-tvg ecdhe key", t => {
|
||||
const priv1 = "YSYA38XH1PH40ZPSEZCXEFX9PH9Q3A2PE19FHM54DMTZ4MAPH9S0";
|
||||
const pub1 = "GNQRNSYF4BT4V0EV0DBXZCHFVQ79ATP0KBJ9EAY18FGSY513A5VG";
|
||||
|
||||
const myPub = nacl.x25519_edwards_keyPair_fromSecretKey(decodeCrock(priv1))
|
||||
t.deepEqual(encodeCrock(myPub), pub1);
|
||||
|
||||
//const myPub1 = nacl.scalarMult.base(decodeCrock(priv1));
|
||||
//t.deepEqual(encodeCrock(myPub1), pub1);
|
||||
|
||||
//const p = nacl.box.keyPair.fromSecretKey(decodeCrock(priv1))
|
||||
//t.deepEqual(encodeCrock(p.publicKey), pub1);
|
||||
|
||||
//const r = nacl.scalarMult(decodeCrock(priv2), decodeCrock(pub1));
|
||||
//t.deepEqual(encodeCrock(nacl.hash(r)), skm);
|
||||
|
||||
//const mySkm = nacl.
|
||||
});
|
||||
|
||||
test("taler-exchange-tvg eddsa key", t => {
|
||||
const priv = "H2JGQ2T3A5WBC5QV3YRFE31AMRGF2F9WPXZ03EM3NS3PYHM80WA0";
|
||||
const pub = "QFGMB2WTPYXMXZRPFYFEM2VMQ028M71JMECF31P3J8VC3SCJ777G";
|
||||
|
||||
const pair = nacl.sign_keyPair_fromSeed(decodeCrock(priv));
|
||||
t.deepEqual(encodeCrock(pair.publicKey), pub);
|
||||
});
|
@ -1,140 +0,0 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2019 GNUnet e.V.
|
||||
|
||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Native implementation of GNU Taler crypto.
|
||||
*/
|
||||
|
||||
let isNode;
|
||||
|
||||
let myGetRandom: (n: number) => ArrayBuffer;
|
||||
|
||||
if (require) {
|
||||
// node.js
|
||||
const cr = require("crypto");
|
||||
myGetRandom = (n: number) => {
|
||||
const buf = cr.randomBytes(n);
|
||||
return Uint8Array.from(buf);
|
||||
}
|
||||
} else {
|
||||
// Browser
|
||||
myGetRandom = (n: number) => {
|
||||
const ret = new Uint8Array(n);
|
||||
window.crypto.getRandomValues(ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
export function getRandomBytes(n: number): ArrayBuffer {
|
||||
return myGetRandom(n);
|
||||
}
|
||||
|
||||
const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
||||
|
||||
class EncodingError extends Error {
|
||||
constructor() {
|
||||
super("Encoding error");
|
||||
Object.setPrototypeOf(this, EncodingError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
function getValue(chr: string): number {
|
||||
let a = chr;
|
||||
switch (chr) {
|
||||
case "O":
|
||||
case "o":
|
||||
a = "0;";
|
||||
break;
|
||||
case "i":
|
||||
case "I":
|
||||
case "l":
|
||||
case "L":
|
||||
a = "1";
|
||||
break;
|
||||
case "u":
|
||||
case "U":
|
||||
a = "V";
|
||||
}
|
||||
|
||||
if (a >= "0" && a <= "9") {
|
||||
return a.charCodeAt(0) - "0".charCodeAt(0);
|
||||
}
|
||||
|
||||
if (a >= "a" && a <= "z") a = a.toUpperCase();
|
||||
let dec = 0;
|
||||
if (a >= "A" && a <= "Z") {
|
||||
if ("I" < a) dec++;
|
||||
if ("L" < a) dec++;
|
||||
if ("O" < a) dec++;
|
||||
if ("U" < a) dec++;
|
||||
return a.charCodeAt(0) - "A".charCodeAt(0) + 10 - dec;
|
||||
}
|
||||
throw new EncodingError();
|
||||
}
|
||||
|
||||
export function encodeCrock(data: ArrayBuffer): string {
|
||||
const dataBytes = new Uint8Array(data);
|
||||
let sb = "";
|
||||
const size = data.byteLength;
|
||||
let bitBuf = 0;
|
||||
let numBits = 0;
|
||||
let pos = 0;
|
||||
while (pos < size || numBits > 0) {
|
||||
if (pos < size && numBits < 5) {
|
||||
const d = dataBytes[pos++];
|
||||
bitBuf = (bitBuf << 8) | d;
|
||||
numBits += 8;
|
||||
}
|
||||
if (numBits < 5) {
|
||||
// zero-padding
|
||||
bitBuf = bitBuf << (5 - numBits);
|
||||
numBits = 5;
|
||||
}
|
||||
const v = (bitBuf >>> (numBits - 5)) & 31;
|
||||
sb += encTable[v];
|
||||
numBits -= 5;
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
||||
export function decodeCrock(encoded: string): Uint8Array {
|
||||
const size = encoded.length;
|
||||
let bitpos = 0;
|
||||
let bitbuf = 0;
|
||||
let readPosition = 0;
|
||||
const outLen = Math.floor((size * 5) / 8);
|
||||
const out = new Uint8Array(outLen);
|
||||
let outPos = 0;
|
||||
|
||||
while (readPosition < size || bitpos > 0) {
|
||||
//println("at position $readPosition with bitpos $bitpos")
|
||||
if (readPosition < size) {
|
||||
const v = getValue(encoded[readPosition++]);
|
||||
bitbuf = (bitbuf << 5) | v;
|
||||
bitpos += 5;
|
||||
}
|
||||
while (bitpos >= 8) {
|
||||
const d = (bitbuf >>> (bitpos - 8)) & 0xff;
|
||||
out[outPos++] = d;
|
||||
bitpos -= 8;
|
||||
}
|
||||
if (readPosition == size && bitpos > 0) {
|
||||
bitbuf = (bitbuf << (8 - bitpos)) & 0xff;
|
||||
bitpos = bitbuf == 0 ? 0 : 8;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
161
src/crypto/talerCrypto-test.ts
Normal file
161
src/crypto/talerCrypto-test.ts
Normal file
@ -0,0 +1,161 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2019 GNUnet e.V.
|
||||
|
||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Imports
|
||||
*/
|
||||
import test from "ava";
|
||||
import {
|
||||
encodeCrock,
|
||||
decodeCrock,
|
||||
ecdheGetPublic,
|
||||
eddsaGetPublic,
|
||||
keyExchangeEddsaEcdhe,
|
||||
keyExchangeEcdheEddsa,
|
||||
rsaBlind,
|
||||
rsaUnblind,
|
||||
rsaVerify,
|
||||
} from "./talerCrypto";
|
||||
import { hmacSha512, sha512, kdf } from "./kdf";
|
||||
import nacl = require("./nacl-fast");
|
||||
|
||||
function hexToBytes(hex: string) {
|
||||
for (var bytes = [], c = 0; c < hex.length; c += 2)
|
||||
bytes.push(parseInt(hex.substr(c, 2), 16));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function bytesToHex(bytes: Uint8Array): string {
|
||||
for (var hex = [], i = 0; i < bytes.length; i++) {
|
||||
var current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];
|
||||
hex.push((current >>> 4).toString(16));
|
||||
hex.push((current & 0xf).toString(16));
|
||||
}
|
||||
return hex.join("");
|
||||
}
|
||||
|
||||
test("encoding", t => {
|
||||
const utf8decoder = new TextDecoder("utf-8");
|
||||
const utf8encoder = new TextEncoder();
|
||||
const s = "Hello, World";
|
||||
const encStr = encodeCrock(utf8encoder.encode(s));
|
||||
const outBuf = decodeCrock(encStr);
|
||||
const sOut = utf8decoder.decode(outBuf);
|
||||
t.deepEqual(s, sOut);
|
||||
});
|
||||
|
||||
test("taler-exchange-tvg hash code", t => {
|
||||
const input = "91JPRV3F5GG4EKJN41A62V35E8";
|
||||
const output =
|
||||
"CW96WR74JS8T53EC8GKSGD49QKH4ZNFTZXDAWMMV5GJ1E4BM6B8GPN5NVHDJ8ZVXNCW7Q4WBYCV61HCA3PZC2YJD850DT29RHHN7ESR";
|
||||
|
||||
const myOutput = encodeCrock(sha512(decodeCrock(input)));
|
||||
|
||||
t.deepEqual(myOutput, output);
|
||||
});
|
||||
|
||||
test("taler-exchange-tvg ecdhe key", t => {
|
||||
const priv1 = "X4T4N0M8PVQXQEBW2BA7049KFSM7J437NSDFC6GDNM3N5J9367A0";
|
||||
const pub1 = "M997P494MS6A95G1P0QYWW2VNPSHSX5Q6JBY5B9YMNYWP0B50X3G";
|
||||
const priv2 = "14A0MMQ64DCV8HE0CS3WBC9DHFJAHXRGV7NEARFJPC5R5E1697E0";
|
||||
const skm =
|
||||
"NXRY2YCY7H9B6KM928ZD55WG964G59YR0CPX041DYXKBZZ85SAWNPQ8B30QRM5FMHYCXJAN0EAADJYWEF1X3PAC2AJN28626TR5A6AR";
|
||||
|
||||
const myPub1 = nacl.scalarMult_base(decodeCrock(priv1));
|
||||
t.deepEqual(encodeCrock(myPub1), pub1);
|
||||
|
||||
const mySkm = nacl.hash(
|
||||
nacl.scalarMult(decodeCrock(priv2), decodeCrock(pub1)),
|
||||
);
|
||||
t.deepEqual(encodeCrock(mySkm), skm);
|
||||
});
|
||||
|
||||
test("taler-exchange-tvg eddsa key", t => {
|
||||
const priv = "9TM70AKDTS57AWY9JK2J4TMBTMW6K62WHHGZWYDG0VM5ABPZKD40";
|
||||
const pub = "8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0";
|
||||
|
||||
const pair = nacl.sign_keyPair_fromSeed(decodeCrock(priv));
|
||||
t.deepEqual(encodeCrock(pair.publicKey), pub);
|
||||
});
|
||||
|
||||
test("taler-exchange-tvg kdf", t => {
|
||||
const salt = "94KPT83PCNS7J83KC5P78Y8";
|
||||
const ikm = "94KPT83MD1JJ0WV5CDS6AX10D5Q70XBM41NPAY90DNGQ8SBJD5GPR";
|
||||
const ctx =
|
||||
"94KPT83141HPYVKMCNW78833D1TPWTSC41GPRWVF41NPWVVQDRG62WS04XMPWSKF4WG6JVH0EHM6A82J8S1G";
|
||||
const outLen = 64;
|
||||
const out =
|
||||
"GTMR4QT05Z9WF5HKVG0WK9RPXGHSMHJNW377G9GJXCA8B0FEKPF4D27RJMSJZYWSQNTBJ5EYVV7ZW18B48Z0JVJJ80RHB706Y96Q358";
|
||||
|
||||
const myOut = kdf(
|
||||
outLen,
|
||||
decodeCrock(ikm),
|
||||
decodeCrock(salt),
|
||||
decodeCrock(ctx),
|
||||
);
|
||||
|
||||
t.deepEqual(encodeCrock(myOut), out);
|
||||
});
|
||||
|
||||
test("taler-exchange-tvg eddsa_ecdh", t => {
|
||||
const priv_ecdhe = "4AFZWMSGTVCHZPQ0R81NWXDCK4N58G7SDBBE5KXE080Y50370JJG";
|
||||
const pub_ecdhe = "FXFN5GPAFTKVPWJDPVXQ87167S8T82T5ZV8CDYC0NH2AE14X0M30";
|
||||
const priv_eddsa = "1KG54M8T3X8BSFSZXCR3SQBSR7Y9P53NX61M864S7TEVMJ2XVPF0";
|
||||
const pub_eddsa = "7BXWKG6N224C57RTDV8XEAHR108HG78NMA995BE8QAT5GC1S7E80";
|
||||
const key_material =
|
||||
"PKZ42Z56SVK2796HG1QYBRJ6ZQM2T9QGA3JA4AAZ8G7CWK9FPX175Q9JE5P0ZAX3HWWPHAQV4DPCK10R9X3SAXHRV0WF06BHEC2ZTKR";
|
||||
|
||||
const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe));
|
||||
t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe);
|
||||
|
||||
const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa));
|
||||
t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa);
|
||||
|
||||
const myKm1 = keyExchangeEddsaEcdhe(
|
||||
decodeCrock(priv_eddsa),
|
||||
decodeCrock(pub_ecdhe),
|
||||
);
|
||||
t.deepEqual(encodeCrock(myKm1), key_material);
|
||||
|
||||
const myKm2 = keyExchangeEcdheEddsa(
|
||||
decodeCrock(priv_ecdhe),
|
||||
decodeCrock(pub_eddsa),
|
||||
);
|
||||
t.deepEqual(encodeCrock(myKm2), key_material);
|
||||
});
|
||||
|
||||
test("taler-exchange-tvg blind signing", t => {
|
||||
const messageHash =
|
||||
"TT1R28D79EJEJ9PC35AQS35CCG85DSXSZ508MV2HS2FN4ME6AHESZX5WP485R8A75KG53FN6F1YNW95008663TKAPWB81420VG17BY8";
|
||||
const rsaPublicKey =
|
||||
"040000Y62RSDDKZXTE7GDVA302ZZR0DY224RSDT6WDWR1XGT8E3YG80XV6TMT3ZCNP8XC84W0N6MSZ0EF8S3YB1JJ2AXY9JQZW3MCA0CG38ER4YE2RY4Q2666DEZSNKT29V6CKZVCDHXSAKY8W6RPEKEQ5YSBYQK23MRK3CQTNNJXQFDKEMRHEC5Y6RDHAC5RJCV8JJ8BF18VPKZ2Q7BB14YN1HJ22H8EZGW0RDGG9YPEWA9183BHEQ651PP81J514TJ9K8DH23AJ50SZFNS429HQ390VRP5E4MQ7RK7ZJXXTSZAQSRTC0QF28P23PD37C17QFQB0BBC54MB8MDH7RW104STG6VN0J22P39JP4EXPVGK5D9AX5W869MDQ6SRD42ZYK5H20227Q8CCWSQ6C3132WP0F0H04002";
|
||||
const bks = "7QD31RPJH0W306RJWBRG646Z2FTA1F89BKSXPDAG7YM0N5Z0B610";
|
||||
const bm =
|
||||
"GA8PC6YH9VF5MW6P2DKTV0W0ZTQ24DZ9EAN5QH3SQXRH7SCZHFMM21ZY05F0BS7MFW8TSEP4SEB280BYP5ACHNQWGE10PCXDDMK7ECXJDPHJ224JBCV4KYNWG6NBR3SC9HK8FXVFX55GFBJFNQHNZGEB8DB0KN9MSVYFDXN45KPMSNY03FVX0JZ0R3YG9XQ8XVGB5SYZCF0QSHWH61MT0Q10CZD2V114BT64D3GD86EJ5S9WBMYG51SDN5CSKEJ734YAJ4HCEWW0RDN8GXA9ZMA18SKVW8T3TTBCPJRF2Y77JGQ08GF35SYGA2HWFV1HGVS8RCTER6GB9SZHRG4T7919H9C1KFAP50G2KSV6X42D6KNJANNSGKQH649TJ00YJQXPHPNFBSS198RY2C243D4B4W";
|
||||
const bs =
|
||||
"5VW0MS5PRBA3W8TPATSTDA2YRFQM1Z7F2DWKQ8ATMZYYY768Q3STZ3HGNVYQ6JB5NKP80G5HGE58616FPA70SX9PTW7EN8EJ23E26FASBWZBP8E2RWQQ5E0F72B2PWRP5ZCA2J3AB3F6P86XK4PZYT64RF94MDGHY0GSDSSBH5YSFB3VM0KVXA52H2Y2G9S85AVCSD3BTMHQRF5BJJ8JE00T4GK70PSTVCGMRKRNA7DGW7GD2F35W55AXF7R2YJC8PAGNSJYWKC3PC75A5N8H69K299AK5PM3CDDHNS4BMRNGF7K49CR4ZBFRXDAWMB3X6T05Q4NKSG0F1KP5JA0XBMF2YJK7KEPRD1EWCHJE44T9YXBTK4W9CV77X7Z9P407ZC6YB3M2ARANZXHJKSM3XC33M";
|
||||
const sig =
|
||||
"PFT6WQJGCM9DE6264DJS6RMG4XDMCDBJKZGSXAF3BEXWZ979Q13NETKK05S1YV91CX3Y034FSS86SSHZTTE8097RRESQP52EKFGTWJXKHZJEQJ49YHMBNQDHW4CFBJECNJSV2PMHWVGXV7HB84R6P0S3ES559HWQX01Q9MYDEGRNHKW87QR2BNSG951D5NQGAKEJ2SSJBE18S6WYAC24FAP8TT8ANECH5371J0DJY0YR0VWAFWVJDV8XQSFXWMJ80N3A80SPSHPYJY3WZZXW63WQ46WHYY56ZSNE5G1RZ5CR0XYV2ECKPM8R0FS58EV16WTRAM1ABBFVNAT3CAEFAZCWP3XHPVBQY5NZVTD5QS2Q8SKJQ2XB30E11CWDN9KTV5CBK4DN72EVG73F3W3BATAKHG";
|
||||
|
||||
const myBm = rsaBlind(decodeCrock(messageHash), decodeCrock(bks), decodeCrock(rsaPublicKey));
|
||||
t.deepEqual(encodeCrock(myBm), bm);
|
||||
|
||||
const mySig = rsaUnblind(decodeCrock(bs), decodeCrock(rsaPublicKey), decodeCrock(bks));
|
||||
t.deepEqual(encodeCrock(mySig), sig);
|
||||
|
||||
const v = rsaVerify(decodeCrock(messageHash), decodeCrock(sig), decodeCrock(rsaPublicKey));
|
||||
t.true(v);
|
||||
});
|
277
src/crypto/talerCrypto.ts
Normal file
277
src/crypto/talerCrypto.ts
Normal file
@ -0,0 +1,277 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2019 GNUnet e.V.
|
||||
|
||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Native implementation of GNU Taler crypto.
|
||||
*/
|
||||
|
||||
import nacl = require("./nacl-fast");
|
||||
import bigint from "big-integer";
|
||||
import { kdf } from "./kdf";
|
||||
|
||||
export function getRandomBytes(n: number): Uint8Array {
|
||||
return nacl.randomBytes(n);
|
||||
}
|
||||
|
||||
const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
||||
|
||||
class EncodingError extends Error {
|
||||
constructor() {
|
||||
super("Encoding error");
|
||||
Object.setPrototypeOf(this, EncodingError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
function getValue(chr: string): number {
|
||||
let a = chr;
|
||||
switch (chr) {
|
||||
case "O":
|
||||
case "o":
|
||||
a = "0;";
|
||||
break;
|
||||
case "i":
|
||||
case "I":
|
||||
case "l":
|
||||
case "L":
|
||||
a = "1";
|
||||
break;
|
||||
case "u":
|
||||
case "U":
|
||||
a = "V";
|
||||
}
|
||||
|
||||
if (a >= "0" && a <= "9") {
|
||||
return a.charCodeAt(0) - "0".charCodeAt(0);
|
||||
}
|
||||
|
||||
if (a >= "a" && a <= "z") a = a.toUpperCase();
|
||||
let dec = 0;
|
||||
if (a >= "A" && a <= "Z") {
|
||||
if ("I" < a) dec++;
|
||||
if ("L" < a) dec++;
|
||||
if ("O" < a) dec++;
|
||||
if ("U" < a) dec++;
|
||||
return a.charCodeAt(0) - "A".charCodeAt(0) + 10 - dec;
|
||||
}
|
||||
throw new EncodingError();
|
||||
}
|
||||
|
||||
export function encodeCrock(data: ArrayBuffer): string {
|
||||
const dataBytes = new Uint8Array(data);
|
||||
let sb = "";
|
||||
const size = data.byteLength;
|
||||
let bitBuf = 0;
|
||||
let numBits = 0;
|
||||
let pos = 0;
|
||||
while (pos < size || numBits > 0) {
|
||||
if (pos < size && numBits < 5) {
|
||||
const d = dataBytes[pos++];
|
||||
bitBuf = (bitBuf << 8) | d;
|
||||
numBits += 8;
|
||||
}
|
||||
if (numBits < 5) {
|
||||
// zero-padding
|
||||
bitBuf = bitBuf << (5 - numBits);
|
||||
numBits = 5;
|
||||
}
|
||||
const v = (bitBuf >>> (numBits - 5)) & 31;
|
||||
sb += encTable[v];
|
||||
numBits -= 5;
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
||||
export function decodeCrock(encoded: string): Uint8Array {
|
||||
const size = encoded.length;
|
||||
let bitpos = 0;
|
||||
let bitbuf = 0;
|
||||
let readPosition = 0;
|
||||
const outLen = Math.floor((size * 5) / 8);
|
||||
const out = new Uint8Array(outLen);
|
||||
let outPos = 0;
|
||||
|
||||
while (readPosition < size || bitpos > 0) {
|
||||
if (readPosition < size) {
|
||||
const v = getValue(encoded[readPosition++]);
|
||||
bitbuf = (bitbuf << 5) | v;
|
||||
bitpos += 5;
|
||||
}
|
||||
while (bitpos >= 8) {
|
||||
const d = (bitbuf >>> (bitpos - 8)) & 0xff;
|
||||
out[outPos++] = d;
|
||||
bitpos -= 8;
|
||||
}
|
||||
if (readPosition == size && bitpos > 0) {
|
||||
bitbuf = (bitbuf << (8 - bitpos)) & 0xff;
|
||||
bitpos = bitbuf == 0 ? 0 : 8;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function eddsaGetPublic(eddsaPriv: Uint8Array): Uint8Array {
|
||||
const pair = nacl.sign_keyPair_fromSeed(eddsaPriv);
|
||||
return pair.publicKey;
|
||||
}
|
||||
|
||||
export function ecdheGetPublic(ecdhePriv: Uint8Array): Uint8Array {
|
||||
return nacl.scalarMult_base(ecdhePriv);
|
||||
}
|
||||
|
||||
export function keyExchangeEddsaEcdhe(eddsaPriv: Uint8Array, ecdhePub: Uint8Array): Uint8Array {
|
||||
const ph = nacl.hash(eddsaPriv);
|
||||
const a = new Uint8Array(32);
|
||||
for (let i = 0; i < 32; i++) {
|
||||
a[i] = ph[i];
|
||||
}
|
||||
const x = nacl.scalarMult(a, ecdhePub);
|
||||
return nacl.hash(x);
|
||||
}
|
||||
|
||||
export function keyExchangeEcdheEddsa(ecdhePriv: Uint8Array, eddsaPub: Uint8Array): Uint8Array {
|
||||
const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub);
|
||||
const x = nacl.scalarMult(ecdhePriv, curve25519Pub);
|
||||
return nacl.hash(x);
|
||||
}
|
||||
|
||||
interface RsaPub {
|
||||
N: bigint.BigInteger;
|
||||
e: bigint.BigInteger;
|
||||
}
|
||||
|
||||
interface RsaBlindingKey {
|
||||
r: bigint.BigInteger;
|
||||
}
|
||||
|
||||
/**
|
||||
* KDF modulo a big integer.
|
||||
*/
|
||||
function kdfMod(
|
||||
n: bigint.BigInteger,
|
||||
ikm: Uint8Array,
|
||||
salt: Uint8Array,
|
||||
info: Uint8Array,
|
||||
): bigint.BigInteger {
|
||||
const nbits = n.bitLength().toJSNumber();
|
||||
const buflen = Math.floor((nbits - 1) / 8 + 1);
|
||||
const mask = (1 << (8 - (buflen * 8 - nbits))) - 1;
|
||||
let counter = 0;
|
||||
while (true) {
|
||||
const ctx = new Uint8Array(info.byteLength + 2);
|
||||
ctx.set(info, 0);
|
||||
ctx[ctx.length - 2] = (counter >>> 8) & 0xFF;
|
||||
ctx[ctx.length - 1] = counter & 0xFF;
|
||||
const buf = kdf(buflen, ikm, salt, ctx);
|
||||
const arr = Array.from(buf);
|
||||
arr[0] = arr[0] & mask;
|
||||
const r = bigint.fromArray(arr, 256, false);
|
||||
if (r.lt(n)) {
|
||||
return r;
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
function stringToBuf(s: string) {
|
||||
const te = new TextEncoder();
|
||||
return te.encode(s);
|
||||
}
|
||||
|
||||
function loadBigInt(arr: Uint8Array) {
|
||||
return bigint.fromArray(Array.from(arr), 256, false);
|
||||
}
|
||||
|
||||
function rsaBlindingKeyDerive(rsaPub: RsaPub, bks: Uint8Array): bigint.BigInteger {
|
||||
const salt = stringToBuf("Blinding KDF extrator HMAC key");
|
||||
const info = stringToBuf("Blinding KDF");
|
||||
return kdfMod(rsaPub.N, bks, salt, info);
|
||||
}
|
||||
|
||||
/*
|
||||
* Test for malicious RSA key.
|
||||
*
|
||||
* Assuming n is an RSA modulous and r is generated using a call to
|
||||
* GNUNET_CRYPTO_kdf_mod_mpi, if gcd(r,n) != 1 then n must be a
|
||||
* malicious RSA key designed to deanomize the user.
|
||||
*
|
||||
* @param r KDF result
|
||||
* @param n RSA modulus of the public key
|
||||
*/
|
||||
function rsaGcdValidate(r: bigint.BigInteger, n: bigint.BigInteger) {
|
||||
const t = bigint.gcd(r, n);
|
||||
if (!t.equals(bigint.one)) {
|
||||
throw Error("malicious RSA public key");
|
||||
}
|
||||
}
|
||||
|
||||
function rsaFullDomainHash(hm: Uint8Array, rsaPub: RsaPub): bigint.BigInteger {
|
||||
const info = stringToBuf("RSA-FDA FTpsW!");
|
||||
const salt = rsaPubEncode(rsaPub);
|
||||
const r = kdfMod(rsaPub.N, hm, salt, info);
|
||||
rsaGcdValidate(r, rsaPub.N);
|
||||
return r;
|
||||
}
|
||||
|
||||
function rsaPubDecode(rsaPub: Uint8Array): RsaPub {
|
||||
const modulusLength = (rsaPub[0] << 8) | rsaPub[1];
|
||||
const exponentLength = (rsaPub[2] << 8) | rsaPub[3];
|
||||
const modulus = rsaPub.slice(4, 4 + modulusLength)
|
||||
const exponent = rsaPub.slice(4 + modulusLength, 4 + modulusLength + exponentLength);
|
||||
const res = {
|
||||
N: loadBigInt(modulus),
|
||||
e: loadBigInt(exponent),
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function rsaPubEncode(rsaPub: RsaPub): Uint8Array {
|
||||
const mb = rsaPub.N.toArray(256).value;
|
||||
const eb = rsaPub.e.toArray(256).value;
|
||||
const out = new Uint8Array(4 + mb.length + eb.length);
|
||||
out[0] = (mb.length >>> 8) & 0xFF;
|
||||
out[1] = mb.length & 0xFF;
|
||||
out[2] = (eb.length >>> 8) & 0xFF;
|
||||
out[3] = eb.length & 0xFF;
|
||||
out.set(mb, 4);
|
||||
out.set(eb, 4 + mb.length);
|
||||
return out;
|
||||
}
|
||||
|
||||
export function rsaBlind(hm: Uint8Array, bks: Uint8Array, rsaPubEnc: Uint8Array): Uint8Array {
|
||||
const rsaPub = rsaPubDecode(rsaPubEnc);
|
||||
const data = rsaFullDomainHash(hm, rsaPub);
|
||||
const r = rsaBlindingKeyDerive(rsaPub, bks);
|
||||
const r_e = r.modPow(rsaPub.e, rsaPub.N);
|
||||
const bm = r_e.multiply(data).mod(rsaPub.N);
|
||||
return new Uint8Array(bm.toArray(256).value);
|
||||
}
|
||||
|
||||
export function rsaUnblind(sig: Uint8Array, rsaPubEnc: Uint8Array, bks: Uint8Array): Uint8Array {
|
||||
const rsaPub = rsaPubDecode(rsaPubEnc);
|
||||
const blinded_s = loadBigInt(sig);
|
||||
const r = rsaBlindingKeyDerive(rsaPub, bks);
|
||||
const r_inv = r.modInv(rsaPub.N);
|
||||
const s = blinded_s.multiply(r_inv).mod(rsaPub.N);
|
||||
return new Uint8Array(s.toArray(256).value);
|
||||
}
|
||||
|
||||
export function rsaVerify(hm: Uint8Array, rsaSig: Uint8Array, rsaPubEnc: Uint8Array): boolean {
|
||||
const rsaPub = rsaPubDecode(rsaPubEnc);
|
||||
const d = rsaFullDomainHash(hm, rsaPub);
|
||||
const sig = loadBigInt(rsaSig);
|
||||
const sig_e = sig.modPow(rsaPub.e, rsaPub.N);
|
||||
return sig_e.equals(d);
|
||||
}
|
@ -329,6 +329,7 @@ export class CommandGroup<GN extends keyof any, TG> {
|
||||
let foundSubcommand: CommandGroup<any, any> | undefined = undefined;
|
||||
const myArgs: any = (parsedArgs[this.argKey] = {});
|
||||
const foundOptions: { [name: string]: boolean } = {};
|
||||
const currentName = this.name ?? progname;
|
||||
for (i = 0; i < unparsedArgs.length; i++) {
|
||||
const argVal = unparsedArgs[i];
|
||||
if (argsTerminated == false) {
|
||||
@ -341,8 +342,7 @@ export class CommandGroup<GN extends keyof any, TG> {
|
||||
const r = splitOpt(opt);
|
||||
const d = this.longOptions[r.key];
|
||||
if (!d) {
|
||||
const n = this.name ?? progname;
|
||||
console.error(`error: unknown option '--${r.key}' for ${n}`);
|
||||
console.error(`error: unknown option '--${r.key}' for ${currentName}`);
|
||||
process.exit(-1);
|
||||
throw Error("not reached");
|
||||
}
|
||||
@ -412,8 +412,7 @@ export class CommandGroup<GN extends keyof any, TG> {
|
||||
} else {
|
||||
const d = this.arguments[posArgIndex];
|
||||
if (!d) {
|
||||
const n = this.name ?? progname;
|
||||
console.error(`error: too many arguments for ${n}`);
|
||||
console.error(`error: too many arguments for ${currentName}`);
|
||||
process.exit(-1);
|
||||
throw Error("not reached");
|
||||
}
|
||||
@ -424,12 +423,11 @@ export class CommandGroup<GN extends keyof any, TG> {
|
||||
|
||||
for (let i = posArgIndex; i < this.arguments.length; i++) {
|
||||
const d = this.arguments[i];
|
||||
const n = this.name ?? progname;
|
||||
if (d.required) {
|
||||
if (d.args.default !== undefined) {
|
||||
myArgs[d.name] = d.args.default;
|
||||
} else {
|
||||
console.error(`error: missing positional argument '${d.name}' for ${n}`);
|
||||
console.error(`error: missing positional argument '${d.name}' for ${currentName}`);
|
||||
process.exit(-1);
|
||||
throw Error("not reached");
|
||||
}
|
||||
@ -465,7 +463,19 @@ export class CommandGroup<GN extends keyof any, TG> {
|
||||
parsedArgs,
|
||||
);
|
||||
} else if (this.myAction) {
|
||||
this.myAction(parsedArgs);
|
||||
let r;
|
||||
try {
|
||||
r = this.myAction(parsedArgs);
|
||||
} catch (e) {
|
||||
console.error(`An error occured while running ${currentName}`);
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
Promise.resolve(r).catch((e) => {
|
||||
console.error(`An error occured while running ${currentName}`);
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
this.printHelp(progname, parents);
|
||||
process.exit(-1);
|
||||
|
@ -82,8 +82,9 @@ export async function runIntegrationTest(args: {
|
||||
throw Error("payment did not succeed");
|
||||
}
|
||||
|
||||
const refreshRes = await myWallet.refreshDirtyCoins();
|
||||
console.log(`waited to refresh ${refreshRes.numRefreshed} coins`);
|
||||
await myWallet.runPending();
|
||||
//const refreshRes = await myWallet.refreshDirtyCoins();
|
||||
//console.log(`waited to refresh ${refreshRes.numRefreshed} coins`);
|
||||
|
||||
myWallet.stop();
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import os = require("os");
|
||||
import fs = require("fs");
|
||||
import { getDefaultNodeWallet, withdrawTestBalance } from "./helpers";
|
||||
import { MerchantBackendConnection } from "./merchant";
|
||||
import { runIntegrationTest } from "./integrationtest";
|
||||
@ -24,6 +25,7 @@ import * as clk from "./clk";
|
||||
import { BridgeIDBFactory, MemoryBackend } from "idb-bridge";
|
||||
import { Logger } from "../logging";
|
||||
import * as Amounts from "../amounts";
|
||||
import { decodeCrock } from "../crypto/talerCrypto";
|
||||
|
||||
const logger = new Logger("taler-wallet-cli.ts");
|
||||
|
||||
@ -254,6 +256,16 @@ const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
|
||||
"Subcommands for advanced operations (only use if you know what you're doing!).",
|
||||
});
|
||||
|
||||
advancedCli
|
||||
.subcommand("decode", "decode", {
|
||||
help: "Decode base32-crockford",
|
||||
})
|
||||
.action(args => {
|
||||
const enc = fs.readFileSync(0, 'utf8');
|
||||
fs.writeFileSync(1, decodeCrock(enc.trim()))
|
||||
});
|
||||
|
||||
|
||||
advancedCli
|
||||
.subcommand("refresh", "force-refresh", {
|
||||
help: "Force a refresh on a coin.",
|
||||
|
@ -33,12 +33,15 @@
|
||||
"src/crypto/cryptoWorker.ts",
|
||||
"src/crypto/emscInterface-test.ts",
|
||||
"src/crypto/emscInterface.ts",
|
||||
"src/crypto/nativeCrypto-test.ts",
|
||||
"src/crypto/nativeCrypto.ts",
|
||||
"src/crypto/kdf.ts",
|
||||
"src/crypto/nacl-fast.ts",
|
||||
"src/crypto/nodeEmscriptenLoader.ts",
|
||||
"src/crypto/nodeProcessWorker.ts",
|
||||
"src/crypto/nodeWorkerEntry.ts",
|
||||
"src/crypto/sha256.ts",
|
||||
"src/crypto/synchronousWorker.ts",
|
||||
"src/crypto/talerCrypto-test.ts",
|
||||
"src/crypto/talerCrypto.ts",
|
||||
"src/db.ts",
|
||||
"src/dbTypes.ts",
|
||||
"src/headless/bank.ts",
|
||||
|
@ -1120,6 +1120,11 @@ bfj@^6.1.1:
|
||||
hoopy "^0.1.4"
|
||||
tryer "^1.0.1"
|
||||
|
||||
big-integer@^1.6.48:
|
||||
version "1.6.48"
|
||||
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e"
|
||||
integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==
|
||||
|
||||
big.js@^5.2.2:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
|
Loading…
Reference in New Issue
Block a user