wallet-core: implement age restriction support
This commit is contained in:
parent
9b85d139bf
commit
a165afa682
@ -1769,7 +1769,7 @@ function crypto_scalarmult_base(q: Uint8Array, n: Uint8Array): number {
|
|||||||
return crypto_scalarmult(q, n, _9);
|
return crypto_scalarmult(q, n, _9);
|
||||||
}
|
}
|
||||||
|
|
||||||
function crypto_scalarmult_noclamp(
|
export function crypto_scalarmult_noclamp(
|
||||||
q: Uint8Array,
|
q: Uint8Array,
|
||||||
n: Uint8Array,
|
n: Uint8Array,
|
||||||
p: Uint8Array,
|
p: Uint8Array,
|
||||||
@ -3033,6 +3033,18 @@ export function crypto_core_ed25519_scalar_add(
|
|||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduce a scalar "s" to "s mod L". The input can be up to 64 bytes long.
|
||||||
|
*/
|
||||||
|
export function crypto_core_ed25519_scalar_reduce(x: Uint8Array): Uint8Array {
|
||||||
|
const len = x.length;
|
||||||
|
const z = new Float64Array(64);
|
||||||
|
for (let i = 0; i < len; i++) z[i] = x[i];
|
||||||
|
const o = new Uint8Array(32);
|
||||||
|
modL(o, z);
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
export function crypto_core_ed25519_scalar_sub(
|
export function crypto_core_ed25519_scalar_sub(
|
||||||
x: Uint8Array,
|
x: Uint8Array,
|
||||||
y: Uint8Array,
|
y: Uint8Array,
|
||||||
@ -3063,11 +3075,7 @@ export function crypto_edx25519_private_key_create_from_seed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function crypto_edx25519_get_public(priv: Uint8Array): Uint8Array {
|
export function crypto_edx25519_get_public(priv: Uint8Array): Uint8Array {
|
||||||
const pub = new Uint8Array(32);
|
return crypto_scalarmult_ed25519_base_noclamp(priv.subarray(0, 32));
|
||||||
if (0 != crypto_scalarmult_base_noclamp(pub.subarray(32), priv)) {
|
|
||||||
throw Error();
|
|
||||||
}
|
|
||||||
return pub;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function crypto_edx25519_sign_detached(
|
export function crypto_edx25519_sign_detached(
|
||||||
@ -3076,19 +3084,16 @@ export function crypto_edx25519_sign_detached(
|
|||||||
pkx: Uint8Array,
|
pkx: Uint8Array,
|
||||||
): Uint8Array {
|
): Uint8Array {
|
||||||
const n: number = m.length;
|
const n: number = m.length;
|
||||||
const d = new Uint8Array(64),
|
const h = new Uint8Array(64);
|
||||||
h = new Uint8Array(64),
|
const r = new Uint8Array(64);
|
||||||
r = new Uint8Array(64);
|
|
||||||
let i, j;
|
let i, j;
|
||||||
const x = new Float64Array(64);
|
const x = new Float64Array(64);
|
||||||
const p = [gf(), gf(), gf(), gf()];
|
const p = [gf(), gf(), gf(), gf()];
|
||||||
|
|
||||||
for (i = 0; i < 64; i++) d[i] = skx[i];
|
|
||||||
|
|
||||||
const sm = new Uint8Array(n + 64);
|
const sm = new Uint8Array(n + 64);
|
||||||
|
|
||||||
for (i = 0; i < n; i++) sm[64 + i] = m[i];
|
for (i = 0; i < n; i++) sm[64 + i] = m[i];
|
||||||
for (i = 0; i < 32; i++) sm[32 + i] = d[32 + i];
|
for (i = 0; i < 32; i++) sm[32 + i] = skx[32 + i];
|
||||||
|
|
||||||
crypto_hash(r, sm.subarray(32), n + 32);
|
crypto_hash(r, sm.subarray(32), n + 32);
|
||||||
reduce(r);
|
reduce(r);
|
||||||
@ -3103,12 +3108,12 @@ export function crypto_edx25519_sign_detached(
|
|||||||
for (i = 0; i < 32; i++) x[i] = r[i];
|
for (i = 0; i < 32; i++) x[i] = r[i];
|
||||||
for (i = 0; i < 32; i++) {
|
for (i = 0; i < 32; i++) {
|
||||||
for (j = 0; j < 32; j++) {
|
for (j = 0; j < 32; j++) {
|
||||||
x[i + j] += h[i] * d[j];
|
x[i + j] += h[i] * skx[j];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
modL(sm.subarray(32), x);
|
modL(sm.subarray(32), x);
|
||||||
return sm.subarray(64);
|
return sm.subarray(0, 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function crypto_edx25519_sign_detached_verify(
|
export function crypto_edx25519_sign_detached_verify(
|
||||||
|
@ -34,6 +34,10 @@ import {
|
|||||||
scalarMultBase25519,
|
scalarMultBase25519,
|
||||||
deriveSecrets,
|
deriveSecrets,
|
||||||
calcRBlind,
|
calcRBlind,
|
||||||
|
Edx25519,
|
||||||
|
getRandomBytes,
|
||||||
|
bigintToNaclArr,
|
||||||
|
bigintFromNaclArr,
|
||||||
} from "./talerCrypto.js";
|
} from "./talerCrypto.js";
|
||||||
import { sha512, kdf } from "./kdf.js";
|
import { sha512, kdf } from "./kdf.js";
|
||||||
import * as nacl from "./nacl-fast.js";
|
import * as nacl from "./nacl-fast.js";
|
||||||
@ -44,6 +48,7 @@ import { initNodePrng } from "./prng-node.js";
|
|||||||
initNodePrng();
|
initNodePrng();
|
||||||
import bigint from "big-integer";
|
import bigint from "big-integer";
|
||||||
import { AssertionError } from "assert";
|
import { AssertionError } from "assert";
|
||||||
|
import BigInteger from "big-integer";
|
||||||
|
|
||||||
test("encoding", (t) => {
|
test("encoding", (t) => {
|
||||||
const s = "Hello, World";
|
const s = "Hello, World";
|
||||||
@ -343,9 +348,86 @@ test("taler CS blind c", async (t) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sig = await csUnblind(bseed, rPub, pub, b, blindsig);
|
const sig = await csUnblind(bseed, rPub, pub, b, blindsig);
|
||||||
t.deepEqual(sig.s, decodeCrock("F4ZKMFW3Q7DFN0N94KAMG2JFFHAC362T0QZ6ZCVZ73RS8P91CR70"));
|
t.deepEqual(
|
||||||
t.deepEqual(sig.rPub, decodeCrock("CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0"));
|
sig.s,
|
||||||
|
decodeCrock("F4ZKMFW3Q7DFN0N94KAMG2JFFHAC362T0QZ6ZCVZ73RS8P91CR70"),
|
||||||
|
);
|
||||||
|
t.deepEqual(
|
||||||
|
sig.rPub,
|
||||||
|
decodeCrock("CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0"),
|
||||||
|
);
|
||||||
|
|
||||||
const res = await csVerify(decodeCrock(msg_hash), sig, pub);
|
const res = await csVerify(decodeCrock(msg_hash), sig, pub);
|
||||||
t.deepEqual(res, true);
|
t.deepEqual(res, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("bigint/nacl conversion", async (t) => {
|
||||||
|
const b1 = BigInteger(42);
|
||||||
|
const n1 = bigintToNaclArr(b1, 32);
|
||||||
|
t.is(n1[0], 42);
|
||||||
|
t.is(n1.length, 32);
|
||||||
|
const b2 = bigintFromNaclArr(n1);
|
||||||
|
t.true(b1.eq(b2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("taler age restriction crypto", async (t) => {
|
||||||
|
const priv1 = await Edx25519.keyCreate();
|
||||||
|
const pub1 = await Edx25519.getPublic(priv1);
|
||||||
|
|
||||||
|
const seed = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
|
const priv2 = await Edx25519.privateKeyDerive(priv1, seed);
|
||||||
|
const pub2 = await Edx25519.publicKeyDerive(pub1, seed);
|
||||||
|
|
||||||
|
const pub2Ref = await Edx25519.getPublic(priv2);
|
||||||
|
|
||||||
|
t.is(pub2, pub2Ref);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("edx signing", async (t) => {
|
||||||
|
const priv1 = await Edx25519.keyCreate();
|
||||||
|
const pub1 = await Edx25519.getPublic(priv1);
|
||||||
|
|
||||||
|
const msg = stringToBytes("hello world");
|
||||||
|
|
||||||
|
const sig = nacl.crypto_edx25519_sign_detached(
|
||||||
|
msg,
|
||||||
|
decodeCrock(priv1),
|
||||||
|
decodeCrock(pub1),
|
||||||
|
);
|
||||||
|
|
||||||
|
t.true(
|
||||||
|
nacl.crypto_edx25519_sign_detached_verify(msg, sig, decodeCrock(pub1)),
|
||||||
|
);
|
||||||
|
|
||||||
|
sig[0]++;
|
||||||
|
|
||||||
|
t.false(
|
||||||
|
nacl.crypto_edx25519_sign_detached_verify(msg, sig, decodeCrock(pub1)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("edx test vector", async (t) => {
|
||||||
|
// Generated by gnunet-crypto-tvg
|
||||||
|
const tv = {
|
||||||
|
operation: "edx25519_derive",
|
||||||
|
priv1_edx:
|
||||||
|
"216KF1XM46K4JN8TX3Z8HNRX1DX4WRMX1BTCQM3KBS83PYKFY1GV6XRNBYRC5YM02HVDX8BDR20V7A27YX4MZJ8X8K0ADPZ43BD1GXG",
|
||||||
|
pub1_edx: "RKGRRG74SZ8PKF8SYG5SSDY8VRCYYGY5N2AKAJCG0103Z3JK6HTG",
|
||||||
|
seed: "EFK7CYT98YWGPNZNHPP84VJZDMXD5A41PP3E94NSAQZXRCAKVVXHAQNXG9XM2MAND2FJ56ZM238KGDCF3B0KCWNZCYKKHKDB56X6QA0",
|
||||||
|
priv2_edx:
|
||||||
|
"JRV3S06REHQV90E4HJA1FAMCVDBZZAZP9C6N2WF01MSR3CD5KM28QM7HTGGAV6MBJZ73QJ8PSZFA0D6YENJ7YT97344FDVVCGVAFNER",
|
||||||
|
pub2_edx: "ZB546ZC7ZP16DB99AMK67WNZ67WZFPWMRY67Y4PZR9YR1D82GVZ0",
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const pub1Prime = await Edx25519.getPublic(tv.priv1_edx);
|
||||||
|
t.is(pub1Prime, tv.pub1_edx);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pub2Prime = await Edx25519.publicKeyDerive(tv.pub1_edx, tv.seed);
|
||||||
|
t.is(pub2Prime, tv.pub2_edx);
|
||||||
|
|
||||||
|
const priv2Prime = await Edx25519.privateKeyDerive(tv.priv1_edx, tv.seed);
|
||||||
|
t.is(priv2Prime, tv.priv2_edx);
|
||||||
|
});
|
||||||
|
@ -27,6 +27,7 @@ import bigint from "big-integer";
|
|||||||
import {
|
import {
|
||||||
Base32String,
|
Base32String,
|
||||||
CoinEnvelope,
|
CoinEnvelope,
|
||||||
|
CoinPublicKeyString,
|
||||||
DenominationPubKey,
|
DenominationPubKey,
|
||||||
DenomKeyType,
|
DenomKeyType,
|
||||||
HashCodeString,
|
HashCodeString,
|
||||||
@ -643,6 +644,17 @@ export function hashCoinEvInner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hashCoinPub(
|
||||||
|
coinPub: CoinPublicKeyString,
|
||||||
|
ach?: HashCodeString,
|
||||||
|
): Uint8Array {
|
||||||
|
if (!ach) {
|
||||||
|
return hash(decodeCrock(coinPub));
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash(typedArrayConcat([decodeCrock(coinPub), decodeCrock(ach)]));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hash a denomination public key.
|
* Hash a denomination public key.
|
||||||
*/
|
*/
|
||||||
@ -652,6 +664,7 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
|
|||||||
const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
|
const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
|
||||||
const uint8ArrayBuf = new Uint8Array(hashInputBuf);
|
const uint8ArrayBuf = new Uint8Array(hashInputBuf);
|
||||||
const dv = new DataView(hashInputBuf);
|
const dv = new DataView(hashInputBuf);
|
||||||
|
logger.info("age_mask", pub.age_mask);
|
||||||
dv.setUint32(0, pub.age_mask ?? 0);
|
dv.setUint32(0, pub.age_mask ?? 0);
|
||||||
dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher));
|
dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher));
|
||||||
uint8ArrayBuf.set(pubBuf, 8);
|
uint8ArrayBuf.set(pubBuf, 8);
|
||||||
@ -705,6 +718,14 @@ export function bufferForUint32(n: number): Uint8Array {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function bufferForUint8(n: number): Uint8Array {
|
||||||
|
const arrBuf = new ArrayBuffer(1);
|
||||||
|
const buf = new Uint8Array(arrBuf);
|
||||||
|
const dv = new DataView(arrBuf);
|
||||||
|
dv.setUint8(0, n);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
export function setupTipPlanchet(
|
export function setupTipPlanchet(
|
||||||
secretSeed: Uint8Array,
|
secretSeed: Uint8Array,
|
||||||
coinNumber: number,
|
coinNumber: number,
|
||||||
@ -753,6 +774,7 @@ export enum TalerSignaturePurpose {
|
|||||||
WALLET_COIN_RECOUP = 1203,
|
WALLET_COIN_RECOUP = 1203,
|
||||||
WALLET_COIN_LINK = 1204,
|
WALLET_COIN_LINK = 1204,
|
||||||
WALLET_COIN_RECOUP_REFRESH = 1206,
|
WALLET_COIN_RECOUP_REFRESH = 1206,
|
||||||
|
WALLET_AGE_ATTESTATION = 1207,
|
||||||
EXCHANGE_CONFIRM_RECOUP = 1039,
|
EXCHANGE_CONFIRM_RECOUP = 1039,
|
||||||
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
|
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
|
||||||
ANASTASIS_POLICY_UPLOAD = 1400,
|
ANASTASIS_POLICY_UPLOAD = 1400,
|
||||||
@ -807,6 +829,25 @@ export type Edx25519PublicKey = FlavorP<string, "Edx25519PublicKey", 32>;
|
|||||||
export type Edx25519PrivateKey = FlavorP<string, "Edx25519PrivateKey", 64>;
|
export type Edx25519PrivateKey = FlavorP<string, "Edx25519PrivateKey", 64>;
|
||||||
export type Edx25519Signature = FlavorP<string, "Edx25519Signature", 64>;
|
export type Edx25519Signature = FlavorP<string, "Edx25519Signature", 64>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a big integer to a fixed-size, little-endian array.
|
||||||
|
*/
|
||||||
|
export function bigintToNaclArr(
|
||||||
|
x: bigint.BigInteger,
|
||||||
|
size: number,
|
||||||
|
): Uint8Array {
|
||||||
|
const byteArr = new Uint8Array(size);
|
||||||
|
const arr = x.toArray(256).value.reverse();
|
||||||
|
byteArr.set(arr, 0);
|
||||||
|
return byteArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bigintFromNaclArr(arr: Uint8Array): bigint.BigInteger {
|
||||||
|
let rev = new Uint8Array(arr);
|
||||||
|
rev = rev.reverse();
|
||||||
|
return bigint.fromArray(Array.from(rev), 256, false);
|
||||||
|
}
|
||||||
|
|
||||||
export namespace Edx25519 {
|
export namespace Edx25519 {
|
||||||
const revL = [
|
const revL = [
|
||||||
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2,
|
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2,
|
||||||
@ -846,9 +887,9 @@ export namespace Edx25519 {
|
|||||||
): Promise<OpaqueData> {
|
): Promise<OpaqueData> {
|
||||||
const res = kdfKw({
|
const res = kdfKw({
|
||||||
outputLength: 64,
|
outputLength: 64,
|
||||||
salt: stringToBytes("edx2559-derivation"),
|
salt: decodeCrock(seed),
|
||||||
ikm: decodeCrock(pub),
|
ikm: decodeCrock(pub),
|
||||||
info: decodeCrock(seed),
|
info: stringToBytes("edx2559-derivation"),
|
||||||
});
|
});
|
||||||
|
|
||||||
return encodeCrock(res);
|
return encodeCrock(res);
|
||||||
@ -860,28 +901,191 @@ export namespace Edx25519 {
|
|||||||
): Promise<Edx25519PrivateKey> {
|
): Promise<Edx25519PrivateKey> {
|
||||||
const pub = await getPublic(priv);
|
const pub = await getPublic(priv);
|
||||||
const privDec = decodeCrock(priv);
|
const privDec = decodeCrock(priv);
|
||||||
const privA = privDec.subarray(0, 32).reverse();
|
const a = bigintFromNaclArr(privDec.subarray(0, 32));
|
||||||
const a = bigint.fromArray(Array.from(privA), 256, false);
|
const factorEnc = await deriveFactor(pub, seed);
|
||||||
|
const factorModL = bigintFromNaclArr(decodeCrock(factorEnc)).mod(L);
|
||||||
|
|
||||||
const factorBuf = await deriveFactor(pub, seed);
|
const aPrime = a.divide(8).multiply(factorModL).mod(L).multiply(8).mod(L);
|
||||||
|
const bPrime = nacl
|
||||||
|
.hash(
|
||||||
|
typedArrayConcat([privDec.subarray(32, 64), decodeCrock(factorEnc)]),
|
||||||
|
)
|
||||||
|
.subarray(0, 32);
|
||||||
|
|
||||||
const factor = bigint.fromArray(Array.from(factorBuf), 256, false);
|
const newPriv = encodeCrock(
|
||||||
|
typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]),
|
||||||
const aPrime = a.divide(8).multiply(factor).multiply(8);
|
|
||||||
|
|
||||||
const bPrime = nacl.hash(
|
|
||||||
typedArrayConcat([privDec.subarray(32, 64), decodeCrock(factorBuf)]),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Uint8Array.from(aPrime.toArray(256).value)
|
return newPriv;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publicKeyDerive(
|
||||||
|
pub: Edx25519PublicKey,
|
||||||
|
seed: OpaqueData,
|
||||||
|
): Promise<Edx25519PublicKey> {
|
||||||
|
const factorEnc = await deriveFactor(pub, seed);
|
||||||
|
const factorReduced = nacl.crypto_core_ed25519_scalar_reduce(
|
||||||
|
decodeCrock(factorEnc),
|
||||||
|
);
|
||||||
|
const res = nacl.crypto_scalarmult_ed25519_noclamp(
|
||||||
|
factorReduced,
|
||||||
|
decodeCrock(pub),
|
||||||
|
);
|
||||||
|
return encodeCrock(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgeCommitment {
|
||||||
|
mask: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public keys, one for each age group specified in the age mask.
|
||||||
|
*/
|
||||||
|
publicKeys: Edx25519PublicKey[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgeProof {
|
||||||
|
/**
|
||||||
|
* Private keys. Typically smaller than the number of public keys,
|
||||||
|
* because we drop private keys from age groups that are restricted.
|
||||||
|
*/
|
||||||
|
privateKeys: Edx25519PrivateKey[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgeCommitmentProof {
|
||||||
|
commitment: AgeCommitment;
|
||||||
|
proof: AgeProof;
|
||||||
|
}
|
||||||
|
|
||||||
|
function invariant(cond: boolean): asserts cond {
|
||||||
|
if (!cond) {
|
||||||
|
throw Error("invariant failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace AgeRestriction {
|
||||||
|
export function hashCommitment(ac: AgeCommitment): HashCodeString {
|
||||||
|
const hc = new nacl.HashState();
|
||||||
|
for (const pub of ac.publicKeys) {
|
||||||
|
hc.update(decodeCrock(pub));
|
||||||
|
}
|
||||||
|
return encodeCrock(hc.finish().subarray(0, 32));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countAgeGroups(mask: number): number {
|
||||||
|
let count = 0;
|
||||||
|
let m = mask;
|
||||||
|
while (m > 0) {
|
||||||
|
count += m & 1;
|
||||||
|
m = m >> 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAgeGroupIndex(mask: number, age: number): number {
|
||||||
|
invariant((mask & 1) === 1);
|
||||||
|
let i = 0;
|
||||||
|
let m = mask;
|
||||||
|
let a = age;
|
||||||
|
while (m > 0) {
|
||||||
|
if (a <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
m = m >> 1;
|
||||||
|
i += m & 1;
|
||||||
|
a--;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ageGroupSpecToMask(ageGroupSpec: string): number {
|
||||||
throw Error("not implemented");
|
throw Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function publicKeyDerive(
|
export async function restrictionCommit(
|
||||||
priv: Edx25519PrivateKey,
|
ageMask: number,
|
||||||
seed: OpaqueData,
|
age: number,
|
||||||
): Promise<Edx25519PublicKey> {
|
): Promise<AgeCommitmentProof> {
|
||||||
throw Error("not implemented")
|
invariant((ageMask & 1) === 1);
|
||||||
|
const numPubs = countAgeGroups(ageMask) - 1;
|
||||||
|
const numPrivs = getAgeGroupIndex(ageMask, age);
|
||||||
|
|
||||||
|
const pubs: Edx25519PublicKey[] = [];
|
||||||
|
const privs: Edx25519PrivateKey[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < numPubs; i++) {
|
||||||
|
const priv = await Edx25519.keyCreate();
|
||||||
|
const pub = await Edx25519.getPublic(priv);
|
||||||
|
pubs.push(pub);
|
||||||
|
if (i < numPrivs) {
|
||||||
|
privs.push(priv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
commitment: {
|
||||||
|
mask: ageMask,
|
||||||
|
publicKeys: pubs,
|
||||||
|
},
|
||||||
|
proof: {
|
||||||
|
privateKeys: privs,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function commitmentDerive(
|
||||||
|
commitmentProof: AgeCommitmentProof,
|
||||||
|
salt: OpaqueData,
|
||||||
|
): Promise<AgeCommitmentProof> {
|
||||||
|
const newPrivs: Edx25519PrivateKey[] = [];
|
||||||
|
const newPubs: Edx25519PublicKey[] = [];
|
||||||
|
|
||||||
|
for (const oldPub of commitmentProof.commitment.publicKeys) {
|
||||||
|
newPubs.push(await Edx25519.publicKeyDerive(oldPub, salt));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const oldPriv of commitmentProof.proof.privateKeys) {
|
||||||
|
newPrivs.push(await Edx25519.privateKeyDerive(oldPriv, salt));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
commitment: {
|
||||||
|
mask: commitmentProof.commitment.mask,
|
||||||
|
publicKeys: newPubs,
|
||||||
|
},
|
||||||
|
proof: {
|
||||||
|
privateKeys: newPrivs,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function commitmentAttest(
|
||||||
|
commitmentProof: AgeCommitmentProof,
|
||||||
|
age: number,
|
||||||
|
): Edx25519Signature {
|
||||||
|
const d = buildSigPS(TalerSignaturePurpose.WALLET_AGE_ATTESTATION)
|
||||||
|
.put(bufferForUint32(commitmentProof.commitment.mask))
|
||||||
|
.put(bufferForUint32(age))
|
||||||
|
.build();
|
||||||
|
const group = getAgeGroupIndex(commitmentProof.commitment.mask, age);
|
||||||
|
if (group === 0) {
|
||||||
|
// No attestation required.
|
||||||
|
return encodeCrock(new Uint8Array(64));
|
||||||
|
}
|
||||||
|
const priv = commitmentProof.proof.privateKeys[group - 1];
|
||||||
|
const pub = commitmentProof.commitment.publicKeys[group - 1];
|
||||||
|
const sig = nacl.crypto_edx25519_sign_detached(
|
||||||
|
d,
|
||||||
|
decodeCrock(priv),
|
||||||
|
decodeCrock(pub),
|
||||||
|
);
|
||||||
|
return encodeCrock(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function commitmentVerify(
|
||||||
|
commitmentProof: AgeCommitmentProof,
|
||||||
|
age: number,
|
||||||
|
): Edx25519Signature {
|
||||||
|
throw Error("not implemented");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ import {
|
|||||||
} from "./time.js";
|
} from "./time.js";
|
||||||
import { codecForAmountString } from "./amounts.js";
|
import { codecForAmountString } from "./amounts.js";
|
||||||
import { strcmp } from "./helpers.js";
|
import { strcmp } from "./helpers.js";
|
||||||
|
import { Edx25519PublicKey } from "./talerCrypto.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Denomination as found in the /keys response from the exchange.
|
* Denomination as found in the /keys response from the exchange.
|
||||||
@ -283,6 +284,10 @@ export interface CoinDepositPermission {
|
|||||||
* URL of the exchange this coin was withdrawn from.
|
* URL of the exchange this coin was withdrawn from.
|
||||||
*/
|
*/
|
||||||
exchange_url: string;
|
exchange_url: string;
|
||||||
|
|
||||||
|
minimum_age_sig?: EddsaSignatureString;
|
||||||
|
|
||||||
|
age_commitment?: Edx25519PublicKey[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -539,6 +544,8 @@ export interface ContractTerms {
|
|||||||
*/
|
*/
|
||||||
max_wire_fee?: string;
|
max_wire_fee?: string;
|
||||||
|
|
||||||
|
minimum_age?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extra data, interpreted by the mechant only.
|
* Extra data, interpreted by the mechant only.
|
||||||
*/
|
*/
|
||||||
@ -957,6 +964,7 @@ export interface ExchangeMeltRequest {
|
|||||||
denom_sig: UnblindedSignature;
|
denom_sig: UnblindedSignature;
|
||||||
rc: string;
|
rc: string;
|
||||||
value_with_fee: AmountString;
|
value_with_fee: AmountString;
|
||||||
|
age_commitment_hash?: HashCodeString;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExchangeMeltResponse {
|
export interface ExchangeMeltResponse {
|
||||||
@ -1122,7 +1130,7 @@ export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
|
|||||||
export interface RsaDenominationPubKey {
|
export interface RsaDenominationPubKey {
|
||||||
readonly cipher: DenomKeyType.Rsa;
|
readonly cipher: DenomKeyType.Rsa;
|
||||||
readonly rsa_public_key: string;
|
readonly rsa_public_key: string;
|
||||||
readonly age_mask?: number;
|
readonly age_mask: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CsDenominationPubKey {
|
export interface CsDenominationPubKey {
|
||||||
@ -1177,12 +1185,14 @@ export const codecForRsaDenominationPubKey = () =>
|
|||||||
buildCodecForObject<RsaDenominationPubKey>()
|
buildCodecForObject<RsaDenominationPubKey>()
|
||||||
.property("cipher", codecForConstString(DenomKeyType.Rsa))
|
.property("cipher", codecForConstString(DenomKeyType.Rsa))
|
||||||
.property("rsa_public_key", codecForString())
|
.property("rsa_public_key", codecForString())
|
||||||
|
.property("age_mask", codecForNumber())
|
||||||
.build("DenominationPubKey");
|
.build("DenominationPubKey");
|
||||||
|
|
||||||
export const codecForCsDenominationPubKey = () =>
|
export const codecForCsDenominationPubKey = () =>
|
||||||
buildCodecForObject<CsDenominationPubKey>()
|
buildCodecForObject<CsDenominationPubKey>()
|
||||||
.property("cipher", codecForConstString(DenomKeyType.ClauseSchnorr))
|
.property("cipher", codecForConstString(DenomKeyType.ClauseSchnorr))
|
||||||
.property("cs_public_key", codecForString())
|
.property("cs_public_key", codecForString())
|
||||||
|
.property("age_mask", codecForNumber())
|
||||||
.build("CsDenominationPubKey");
|
.build("CsDenominationPubKey");
|
||||||
|
|
||||||
export const codecForBankWithdrawalOperationPostResponse =
|
export const codecForBankWithdrawalOperationPostResponse =
|
||||||
@ -1312,6 +1322,7 @@ export const codecForContractTerms = (): Codec<ContractTerms> =>
|
|||||||
.property("exchanges", codecForList(codecForExchangeHandle()))
|
.property("exchanges", codecForList(codecForExchangeHandle()))
|
||||||
.property("products", codecOptional(codecForList(codecForProduct())))
|
.property("products", codecOptional(codecForList(codecForProduct())))
|
||||||
.property("extra", codecForAny())
|
.property("extra", codecForAny())
|
||||||
|
.property("minimum_age", codecOptional(codecForNumber()))
|
||||||
.build("ContractTerms");
|
.build("ContractTerms");
|
||||||
|
|
||||||
export const codecForMerchantRefundPermission =
|
export const codecForMerchantRefundPermission =
|
||||||
@ -1717,6 +1728,13 @@ export interface ExchangeRefreshRevealRequest {
|
|||||||
transfer_pub: EddsaPublicKeyString;
|
transfer_pub: EddsaPublicKeyString;
|
||||||
|
|
||||||
link_sigs: EddsaSignatureString[];
|
link_sigs: EddsaSignatureString[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iff the corresponding denomination has support for age restriction,
|
||||||
|
* the client MUST provide the original age commitment, i.e. the vector
|
||||||
|
* of public keys.
|
||||||
|
*/
|
||||||
|
old_age_commitment?: Edx25519PublicKey[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DepositSuccess {
|
export interface DepositSuccess {
|
||||||
|
@ -47,6 +47,7 @@ import {
|
|||||||
codecForConstString,
|
codecForConstString,
|
||||||
codecForAny,
|
codecForAny,
|
||||||
buildCodecForUnion,
|
buildCodecForUnion,
|
||||||
|
codecForNumber,
|
||||||
} from "./codec.js";
|
} from "./codec.js";
|
||||||
import {
|
import {
|
||||||
AmountString,
|
AmountString,
|
||||||
@ -61,6 +62,7 @@ import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js";
|
|||||||
import { BackupRecovery } from "./backupTypes.js";
|
import { BackupRecovery } from "./backupTypes.js";
|
||||||
import { PaytoUri } from "./payto.js";
|
import { PaytoUri } from "./payto.js";
|
||||||
import { TalerErrorCode } from "./taler-error-codes.js";
|
import { TalerErrorCode } from "./taler-error-codes.js";
|
||||||
|
import { AgeCommitmentProof } from "./talerCrypto.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response for the create reserve request to the wallet.
|
* Response for the create reserve request to the wallet.
|
||||||
@ -218,6 +220,8 @@ export interface CreateReserveRequest {
|
|||||||
* from this reserve, only used for testing.
|
* from this reserve, only used for testing.
|
||||||
*/
|
*/
|
||||||
forcedDenomSel?: ForcedDenomSel;
|
forcedDenomSel?: ForcedDenomSel;
|
||||||
|
|
||||||
|
restrictAge?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForCreateReserveRequest = (): Codec<CreateReserveRequest> =>
|
export const codecForCreateReserveRequest = (): Codec<CreateReserveRequest> =>
|
||||||
@ -489,6 +493,7 @@ export interface WithdrawalPlanchet {
|
|||||||
coinEv: CoinEnvelope;
|
coinEv: CoinEnvelope;
|
||||||
coinValue: AmountJson;
|
coinValue: AmountJson;
|
||||||
coinEvHash: string;
|
coinEvHash: string;
|
||||||
|
ageCommitmentProof?: AgeCommitmentProof;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlanchetCreationRequest {
|
export interface PlanchetCreationRequest {
|
||||||
@ -499,6 +504,7 @@ export interface PlanchetCreationRequest {
|
|||||||
denomPub: DenominationPubKey;
|
denomPub: DenominationPubKey;
|
||||||
reservePub: string;
|
reservePub: string;
|
||||||
reservePriv: string;
|
reservePriv: string;
|
||||||
|
restrictAge?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -545,6 +551,10 @@ export interface DepositInfo {
|
|||||||
denomKeyType: DenomKeyType;
|
denomKeyType: DenomKeyType;
|
||||||
denomPubHash: string;
|
denomPubHash: string;
|
||||||
denomSig: UnblindedSignature;
|
denomSig: UnblindedSignature;
|
||||||
|
|
||||||
|
requiredMinimumAge?: number;
|
||||||
|
|
||||||
|
ageCommitmentProof?: AgeCommitmentProof;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExchangesListRespose {
|
export interface ExchangesListRespose {
|
||||||
@ -728,12 +738,14 @@ export const codecForAcceptManualWithdrawalRequet =
|
|||||||
export interface GetWithdrawalDetailsForAmountRequest {
|
export interface GetWithdrawalDetailsForAmountRequest {
|
||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
amount: string;
|
amount: string;
|
||||||
|
restrictAge?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AcceptBankIntegratedWithdrawalRequest {
|
export interface AcceptBankIntegratedWithdrawalRequest {
|
||||||
talerWithdrawUri: string;
|
talerWithdrawUri: string;
|
||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
forcedDenomSel?: ForcedDenomSel;
|
forcedDenomSel?: ForcedDenomSel;
|
||||||
|
restrictAge?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForAcceptBankIntegratedWithdrawalRequest =
|
export const codecForAcceptBankIntegratedWithdrawalRequest =
|
||||||
@ -742,6 +754,7 @@ export const codecForAcceptBankIntegratedWithdrawalRequest =
|
|||||||
.property("exchangeBaseUrl", codecForString())
|
.property("exchangeBaseUrl", codecForString())
|
||||||
.property("talerWithdrawUri", codecForString())
|
.property("talerWithdrawUri", codecForString())
|
||||||
.property("forcedDenomSel", codecForAny())
|
.property("forcedDenomSel", codecForAny())
|
||||||
|
.property("restrictAge", codecOptional(codecForNumber()))
|
||||||
.build("AcceptBankIntegratedWithdrawalRequest");
|
.build("AcceptBankIntegratedWithdrawalRequest");
|
||||||
|
|
||||||
export const codecForGetWithdrawalDetailsForAmountRequest =
|
export const codecForGetWithdrawalDetailsForAmountRequest =
|
||||||
@ -774,11 +787,13 @@ export const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> =>
|
|||||||
|
|
||||||
export interface GetWithdrawalDetailsForUriRequest {
|
export interface GetWithdrawalDetailsForUriRequest {
|
||||||
talerWithdrawUri: string;
|
talerWithdrawUri: string;
|
||||||
|
restrictAge?: number;
|
||||||
}
|
}
|
||||||
export const codecForGetWithdrawalDetailsForUri =
|
export const codecForGetWithdrawalDetailsForUri =
|
||||||
(): Codec<GetWithdrawalDetailsForUriRequest> =>
|
(): Codec<GetWithdrawalDetailsForUriRequest> =>
|
||||||
buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
|
buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
|
||||||
.property("talerWithdrawUri", codecForString())
|
.property("talerWithdrawUri", codecForString())
|
||||||
|
.property("restrictAge", codecOptional(codecForNumber()))
|
||||||
.build("GetWithdrawalDetailsForUriRequest");
|
.build("GetWithdrawalDetailsForUriRequest");
|
||||||
|
|
||||||
export interface ListKnownBankAccountsRequest {
|
export interface ListKnownBankAccountsRequest {
|
||||||
|
@ -24,6 +24,7 @@ export interface CoinCoinfigCommon {
|
|||||||
feeDeposit: string;
|
feeDeposit: string;
|
||||||
feeRefresh: string;
|
feeRefresh: string;
|
||||||
feeRefund: string;
|
feeRefund: string;
|
||||||
|
ageRestricted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CoinConfigRsa extends CoinCoinfigCommon {
|
export interface CoinConfigRsa extends CoinCoinfigCommon {
|
||||||
|
@ -430,6 +430,9 @@ function setCoin(config: Configuration, c: CoinConfig) {
|
|||||||
config.setString(s, "fee_withdraw", c.feeWithdraw);
|
config.setString(s, "fee_withdraw", c.feeWithdraw);
|
||||||
config.setString(s, "fee_refresh", c.feeRefresh);
|
config.setString(s, "fee_refresh", c.feeRefresh);
|
||||||
config.setString(s, "fee_refund", c.feeRefund);
|
config.setString(s, "fee_refund", c.feeRefund);
|
||||||
|
if (c.ageRestricted) {
|
||||||
|
config.setString(s, "age_restricted", "yes");
|
||||||
|
}
|
||||||
if (c.cipher === "RSA") {
|
if (c.cipher === "RSA") {
|
||||||
config.setString(s, "rsa_keysize", `${c.rsaKeySize}`);
|
config.setString(s, "rsa_keysize", `${c.rsaKeySize}`);
|
||||||
config.setString(s, "cipher", "RSA");
|
config.setString(s, "cipher", "RSA");
|
||||||
@ -1112,6 +1115,17 @@ export class ExchangeService implements ExchangeServiceInterface {
|
|||||||
config.write(this.configFilename);
|
config.write(this.configFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enableAgeRestrictions(maskStr: string) {
|
||||||
|
const config = Configuration.load(this.configFilename);
|
||||||
|
config.setString("exchange-extension-age_restriction", "enabled", "yes");
|
||||||
|
config.setString(
|
||||||
|
"exchange-extension-age_restriction",
|
||||||
|
"age_groups",
|
||||||
|
maskStr,
|
||||||
|
);
|
||||||
|
config.write(this.configFilename);
|
||||||
|
}
|
||||||
|
|
||||||
get masterPub() {
|
get masterPub() {
|
||||||
return encodeCrock(this.keyPair.eddsaPub);
|
return encodeCrock(this.keyPair.eddsaPub);
|
||||||
}
|
}
|
||||||
@ -1645,8 +1659,14 @@ export class MerchantService implements MerchantServiceInterface {
|
|||||||
await exec(`taler-merchant-dbinit -c "${this.configFilename}"`);
|
await exec(`taler-merchant-dbinit -c "${this.configFilename}"`);
|
||||||
|
|
||||||
this.proc = this.globalState.spawnService(
|
this.proc = this.globalState.spawnService(
|
||||||
"taler-merchant-httpd",
|
"valgrind",
|
||||||
["-LDEBUG", "-c", this.configFilename, ...this.timetravelArgArr],
|
[
|
||||||
|
"taler-merchant-httpd",
|
||||||
|
"-LDEBUG",
|
||||||
|
"-c",
|
||||||
|
this.configFilename,
|
||||||
|
...this.timetravelArgArr,
|
||||||
|
],
|
||||||
`merchant-${this.merchantConfig.name}`,
|
`merchant-${this.merchantConfig.name}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1848,6 +1868,9 @@ export async function runTestWithState(
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("FATAL: test failed with exception", e);
|
console.error("FATAL: test failed with exception", e);
|
||||||
|
if (e instanceof TalerError) {
|
||||||
|
console.error(`error detail: ${j2s(e.errorDetail)}`);
|
||||||
|
}
|
||||||
status = "fail";
|
status = "fail";
|
||||||
} finally {
|
} finally {
|
||||||
await gc.shutdown();
|
await gc.shutdown();
|
||||||
|
@ -65,6 +65,13 @@ export interface SimpleTestEnvironment {
|
|||||||
wallet: WalletCli;
|
wallet: WalletCli;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EnvOptions {
|
||||||
|
/**
|
||||||
|
* If provided, enable age restrictions with the specified age mask string.
|
||||||
|
*/
|
||||||
|
ageMaskSpec?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a test case with a simple TESTKUDOS Taler environment, consisting
|
* Run a test case with a simple TESTKUDOS Taler environment, consisting
|
||||||
* of one exchange, one bank and one merchant.
|
* of one exchange, one bank and one merchant.
|
||||||
@ -72,6 +79,7 @@ export interface SimpleTestEnvironment {
|
|||||||
export async function createSimpleTestkudosEnvironment(
|
export async function createSimpleTestkudosEnvironment(
|
||||||
t: GlobalTestState,
|
t: GlobalTestState,
|
||||||
coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
|
coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
|
||||||
|
opts: EnvOptions = {},
|
||||||
): Promise<SimpleTestEnvironment> {
|
): Promise<SimpleTestEnvironment> {
|
||||||
const db = await setupDb(t);
|
const db = await setupDb(t);
|
||||||
|
|
||||||
@ -108,7 +116,17 @@ export async function createSimpleTestkudosEnvironment(
|
|||||||
|
|
||||||
await bank.pingUntilAvailable();
|
await bank.pingUntilAvailable();
|
||||||
|
|
||||||
exchange.addCoinConfigList(coinConfig);
|
const ageMaskSpec = opts.ageMaskSpec;
|
||||||
|
|
||||||
|
if (ageMaskSpec) {
|
||||||
|
exchange.enableAgeRestrictions(ageMaskSpec);
|
||||||
|
// Enable age restriction for all coins.
|
||||||
|
exchange.addCoinConfigList(
|
||||||
|
coinConfig.map((x) => ({ ...x, ageRestricted: true })),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
exchange.addCoinConfigList(coinConfig);
|
||||||
|
}
|
||||||
|
|
||||||
await exchange.start();
|
await exchange.start();
|
||||||
await exchange.pingUntilAvailable();
|
await exchange.pingUntilAvailable();
|
||||||
@ -259,6 +277,7 @@ export async function startWithdrawViaBank(
|
|||||||
bank: BankService;
|
bank: BankService;
|
||||||
exchange: ExchangeServiceInterface;
|
exchange: ExchangeServiceInterface;
|
||||||
amount: AmountString;
|
amount: AmountString;
|
||||||
|
restrictAge?: number;
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { wallet, bank, exchange, amount } = p;
|
const { wallet, bank, exchange, amount } = p;
|
||||||
@ -270,6 +289,7 @@ export async function startWithdrawViaBank(
|
|||||||
|
|
||||||
await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
|
await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
|
||||||
talerWithdrawUri: wop.taler_withdraw_uri,
|
talerWithdrawUri: wop.taler_withdraw_uri,
|
||||||
|
restrictAge: p.restrictAge,
|
||||||
});
|
});
|
||||||
|
|
||||||
await wallet.runPending();
|
await wallet.runPending();
|
||||||
@ -279,6 +299,7 @@ export async function startWithdrawViaBank(
|
|||||||
await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
|
await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
|
||||||
exchangeBaseUrl: exchange.baseUrl,
|
exchangeBaseUrl: exchange.baseUrl,
|
||||||
talerWithdrawUri: wop.taler_withdraw_uri,
|
talerWithdrawUri: wop.taler_withdraw_uri,
|
||||||
|
restrictAge: p.restrictAge,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Confirm it
|
// Confirm it
|
||||||
@ -299,6 +320,7 @@ export async function withdrawViaBank(
|
|||||||
bank: BankService;
|
bank: BankService;
|
||||||
exchange: ExchangeServiceInterface;
|
exchange: ExchangeServiceInterface;
|
||||||
amount: AmountString;
|
amount: AmountString;
|
||||||
|
restrictAge?: number;
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { wallet } = p;
|
const { wallet } = p;
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022 Taler Systems S.A.
|
||||||
|
|
||||||
|
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 { defaultCoinConfig } from "../harness/denomStructures.js";
|
||||||
|
import { GlobalTestState } from "../harness/harness.js";
|
||||||
|
import {
|
||||||
|
createSimpleTestkudosEnvironment,
|
||||||
|
withdrawViaBank,
|
||||||
|
makeTestPayment,
|
||||||
|
} from "../harness/helpers.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run test for basic, bank-integrated withdrawal and payment.
|
||||||
|
*/
|
||||||
|
export async function runAgeRestrictionsTest(t: GlobalTestState) {
|
||||||
|
// Set up test environment
|
||||||
|
|
||||||
|
const { wallet, bank, exchange, merchant } =
|
||||||
|
await createSimpleTestkudosEnvironment(
|
||||||
|
t,
|
||||||
|
defaultCoinConfig.map((x) => x("TESTKUDOS")),
|
||||||
|
{
|
||||||
|
ageMaskSpec: "8:10:12:14:16:18:21",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Withdraw digital cash into the wallet.
|
||||||
|
|
||||||
|
await withdrawViaBank(t, {
|
||||||
|
wallet,
|
||||||
|
bank,
|
||||||
|
exchange,
|
||||||
|
amount: "TESTKUDOS:20",
|
||||||
|
restrictAge: 13,
|
||||||
|
});
|
||||||
|
|
||||||
|
const order = {
|
||||||
|
summary: "Buy me!",
|
||||||
|
amount: "TESTKUDOS:5",
|
||||||
|
fulfillment_url: "taler://fulfillment-success/thx",
|
||||||
|
minimum_age: 9,
|
||||||
|
};
|
||||||
|
|
||||||
|
await makeTestPayment(t, { wallet, merchant, order });
|
||||||
|
await wallet.runUntilDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
runAgeRestrictionsTest.suites = ["wallet"];
|
@ -25,6 +25,7 @@ import {
|
|||||||
shouldLingerInTest,
|
shouldLingerInTest,
|
||||||
TestRunResult,
|
TestRunResult,
|
||||||
} from "../harness/harness.js";
|
} from "../harness/harness.js";
|
||||||
|
import { runAgeRestrictionsTest } from "./test-age-restrictions.js";
|
||||||
import { runBankApiTest } from "./test-bank-api";
|
import { runBankApiTest } from "./test-bank-api";
|
||||||
import { runClaimLoopTest } from "./test-claim-loop";
|
import { runClaimLoopTest } from "./test-claim-loop";
|
||||||
import { runClauseSchnorrTest } from "./test-clause-schnorr.js";
|
import { runClauseSchnorrTest } from "./test-clause-schnorr.js";
|
||||||
@ -103,6 +104,7 @@ interface TestMainFunction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allTests: TestMainFunction[] = [
|
const allTests: TestMainFunction[] = [
|
||||||
|
runAgeRestrictionsTest,
|
||||||
runBankApiTest,
|
runBankApiTest,
|
||||||
runClaimLoopTest,
|
runClaimLoopTest,
|
||||||
runClauseSchnorrTest,
|
runClauseSchnorrTest,
|
||||||
|
@ -69,6 +69,10 @@ import {
|
|||||||
kdf,
|
kdf,
|
||||||
ecdheGetPublic,
|
ecdheGetPublic,
|
||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
|
AgeCommitmentProof,
|
||||||
|
AgeRestriction,
|
||||||
|
hashCoinPub,
|
||||||
|
HashCodeString,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import bigint from "big-integer";
|
import bigint from "big-integer";
|
||||||
import { DenominationRecord, TipCoinSource, WireFee } from "../db.js";
|
import { DenominationRecord, TipCoinSource, WireFee } from "../db.js";
|
||||||
@ -82,7 +86,7 @@ import {
|
|||||||
SignTrackTransactionRequest,
|
SignTrackTransactionRequest,
|
||||||
} from "./cryptoTypes.js";
|
} from "./cryptoTypes.js";
|
||||||
|
|
||||||
//const logger = new Logger("cryptoImplementation.ts");
|
const logger = new Logger("cryptoImplementation.ts");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for (asynchronous) cryptographic operations that
|
* Interface for (asynchronous) cryptographic operations that
|
||||||
@ -547,12 +551,34 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
const denomPub = req.denomPub;
|
const denomPub = req.denomPub;
|
||||||
if (denomPub.cipher === DenomKeyType.Rsa) {
|
if (denomPub.cipher === DenomKeyType.Rsa) {
|
||||||
const reservePub = decodeCrock(req.reservePub);
|
const reservePub = decodeCrock(req.reservePub);
|
||||||
const denomPubRsa = decodeCrock(denomPub.rsa_public_key);
|
|
||||||
const derivedPlanchet = await tci.setupWithdrawalPlanchet(tci, {
|
const derivedPlanchet = await tci.setupWithdrawalPlanchet(tci, {
|
||||||
coinNumber: req.coinIndex,
|
coinNumber: req.coinIndex,
|
||||||
secretSeed: req.secretSeed,
|
secretSeed: req.secretSeed,
|
||||||
});
|
});
|
||||||
const coinPubHash = hash(decodeCrock(derivedPlanchet.coinPub));
|
|
||||||
|
let maybeAcp: AgeCommitmentProof | undefined = undefined;
|
||||||
|
let maybeAgeCommitmentHash: string | undefined = undefined;
|
||||||
|
if (req.restrictAge) {
|
||||||
|
if (denomPub.age_mask === 0) {
|
||||||
|
throw Error(
|
||||||
|
"requested age restriction for a denomination that does not support age restriction",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
logger.info("creating age-restricted planchet");
|
||||||
|
maybeAcp = await AgeRestriction.restrictionCommit(
|
||||||
|
denomPub.age_mask,
|
||||||
|
req.restrictAge,
|
||||||
|
);
|
||||||
|
maybeAgeCommitmentHash = AgeRestriction.hashCommitment(
|
||||||
|
maybeAcp.commitment,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const coinPubHash = hashCoinPub(
|
||||||
|
derivedPlanchet.coinPub,
|
||||||
|
maybeAgeCommitmentHash,
|
||||||
|
);
|
||||||
|
|
||||||
const blindResp = await tci.rsaBlind(tci, {
|
const blindResp = await tci.rsaBlind(tci, {
|
||||||
bks: derivedPlanchet.bks,
|
bks: derivedPlanchet.bks,
|
||||||
hm: encodeCrock(coinPubHash),
|
hm: encodeCrock(coinPubHash),
|
||||||
@ -589,6 +615,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
reservePub: encodeCrock(reservePub),
|
reservePub: encodeCrock(reservePub),
|
||||||
withdrawSig: sigResult.sig,
|
withdrawSig: sigResult.sig,
|
||||||
coinEvHash: encodeCrock(evHash),
|
coinEvHash: encodeCrock(evHash),
|
||||||
|
ageCommitmentProof: maybeAcp,
|
||||||
};
|
};
|
||||||
return planchet;
|
return planchet;
|
||||||
} else {
|
} else {
|
||||||
@ -880,7 +907,23 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
): Promise<CoinDepositPermission> {
|
): Promise<CoinDepositPermission> {
|
||||||
// FIXME: put extensions here if used
|
// FIXME: put extensions here if used
|
||||||
const hExt = new Uint8Array(64);
|
const hExt = new Uint8Array(64);
|
||||||
const hAgeCommitment = new Uint8Array(32);
|
let hAgeCommitment: Uint8Array;
|
||||||
|
let maybeAgeCommitmentHash: string | undefined = undefined;
|
||||||
|
let minimumAgeSig: string | undefined = undefined;
|
||||||
|
if (depositInfo.ageCommitmentProof) {
|
||||||
|
const ach = AgeRestriction.hashCommitment(
|
||||||
|
depositInfo.ageCommitmentProof.commitment,
|
||||||
|
);
|
||||||
|
maybeAgeCommitmentHash = ach;
|
||||||
|
hAgeCommitment = decodeCrock(ach);
|
||||||
|
minimumAgeSig = AgeRestriction.commitmentAttest(
|
||||||
|
depositInfo.ageCommitmentProof,
|
||||||
|
depositInfo.requiredMinimumAge!,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// All zeros.
|
||||||
|
hAgeCommitment = new Uint8Array(32);
|
||||||
|
}
|
||||||
let d: Uint8Array;
|
let d: Uint8Array;
|
||||||
if (depositInfo.denomKeyType === DenomKeyType.Rsa) {
|
if (depositInfo.denomKeyType === DenomKeyType.Rsa) {
|
||||||
d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT)
|
d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT)
|
||||||
@ -914,6 +957,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
cipher: DenomKeyType.Rsa,
|
cipher: DenomKeyType.Rsa,
|
||||||
rsa_signature: depositInfo.denomSig.rsa_signature,
|
rsa_signature: depositInfo.denomSig.rsa_signature,
|
||||||
},
|
},
|
||||||
|
age_commitment: depositInfo.ageCommitmentProof?.commitment.publicKeys,
|
||||||
|
minimum_age_sig: minimumAgeSig,
|
||||||
};
|
};
|
||||||
return s;
|
return s;
|
||||||
} else {
|
} else {
|
||||||
@ -999,10 +1044,19 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
coinNumber: coinIndex,
|
coinNumber: coinIndex,
|
||||||
transferSecret: transferSecretRes.h,
|
transferSecret: transferSecretRes.h,
|
||||||
});
|
});
|
||||||
|
let newAc: AgeCommitmentProof | undefined = undefined;
|
||||||
|
let newAch: HashCodeString | undefined = undefined;
|
||||||
|
if (req.meltCoinAgeCommitmentProof) {
|
||||||
|
newAc = await AgeRestriction.commitmentDerive(
|
||||||
|
req.meltCoinAgeCommitmentProof,
|
||||||
|
transferSecretRes.h,
|
||||||
|
);
|
||||||
|
newAch = AgeRestriction.hashCommitment(newAc.commitment);
|
||||||
|
}
|
||||||
coinPriv = decodeCrock(fresh.coinPriv);
|
coinPriv = decodeCrock(fresh.coinPriv);
|
||||||
coinPub = decodeCrock(fresh.coinPub);
|
coinPub = decodeCrock(fresh.coinPub);
|
||||||
blindingFactor = decodeCrock(fresh.bks);
|
blindingFactor = decodeCrock(fresh.bks);
|
||||||
const coinPubHash = hash(coinPub);
|
const coinPubHash = hashCoinPub(fresh.coinPub, newAch);
|
||||||
if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) {
|
if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) {
|
||||||
throw Error("unsupported cipher, can't create refresh session");
|
throw Error("unsupported cipher, can't create refresh session");
|
||||||
}
|
}
|
||||||
@ -1035,8 +1089,16 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
|||||||
|
|
||||||
const sessionHash = sessionHc.finish();
|
const sessionHash = sessionHc.finish();
|
||||||
let confirmData: Uint8Array;
|
let confirmData: Uint8Array;
|
||||||
// FIXME: fill in age commitment
|
let hAgeCommitment: Uint8Array;
|
||||||
const hAgeCommitment = new Uint8Array(32);
|
if (req.meltCoinAgeCommitmentProof) {
|
||||||
|
hAgeCommitment = decodeCrock(
|
||||||
|
AgeRestriction.hashCommitment(
|
||||||
|
req.meltCoinAgeCommitmentProof.commitment,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
hAgeCommitment = new Uint8Array(32);
|
||||||
|
}
|
||||||
confirmData = buildSigPS(TalerSignaturePurpose.WALLET_COIN_MELT)
|
confirmData = buildSigPS(TalerSignaturePurpose.WALLET_COIN_MELT)
|
||||||
.put(sessionHash)
|
.put(sessionHash)
|
||||||
.put(decodeCrock(meltCoinDenomPubHash))
|
.put(decodeCrock(meltCoinDenomPubHash))
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
AgeCommitmentProof,
|
||||||
AmountJson,
|
AmountJson,
|
||||||
CoinEnvelope,
|
CoinEnvelope,
|
||||||
DenominationPubKey,
|
DenominationPubKey,
|
||||||
@ -55,6 +56,7 @@ export interface DeriveRefreshSessionRequest {
|
|||||||
meltCoinPub: string;
|
meltCoinPub: string;
|
||||||
meltCoinPriv: string;
|
meltCoinPriv: string;
|
||||||
meltCoinDenomPubHash: string;
|
meltCoinDenomPubHash: string;
|
||||||
|
meltCoinAgeCommitmentProof?: AgeCommitmentProof;
|
||||||
newCoinDenoms: RefreshNewDenomInfo[];
|
newCoinDenoms: RefreshNewDenomInfo[];
|
||||||
feeRefresh: AmountJson;
|
feeRefresh: AmountJson;
|
||||||
}
|
}
|
||||||
|
@ -321,9 +321,9 @@ export class CryptoDispatcher {
|
|||||||
return new Promise<T>((resolve, reject) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
let timedOut = false;
|
let timedOut = false;
|
||||||
const timeout = timer.after(5000, () => {
|
const timeout = timer.after(5000, () => {
|
||||||
logger.warn("crypto RPC call timed out");
|
logger.warn(`crypto RPC call ('${operation}') timed out`);
|
||||||
timedOut = true;
|
timedOut = true;
|
||||||
reject(new Error("crypto RPC call timed out"));
|
reject(new Error(`crypto RPC call ('${operation}') timed out`));
|
||||||
});
|
});
|
||||||
p.then((x) => {
|
p.then((x) => {
|
||||||
if (timedOut) {
|
if (timedOut) {
|
||||||
|
@ -40,6 +40,7 @@ import {
|
|||||||
CoinEnvelope,
|
CoinEnvelope,
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
TalerProtocolDuration,
|
TalerProtocolDuration,
|
||||||
|
AgeCommitmentProof,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { RetryInfo } from "./util/retries.js";
|
import { RetryInfo } from "./util/retries.js";
|
||||||
import { PayCoinSelection } from "./util/coinSelection.js";
|
import { PayCoinSelection } from "./util/coinSelection.js";
|
||||||
@ -188,6 +189,15 @@ export interface ReserveRecord {
|
|||||||
*/
|
*/
|
||||||
bankInfo?: ReserveBankInfo;
|
bankInfo?: ReserveBankInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restrict withdrawals from this reserve to this age.
|
||||||
|
*/
|
||||||
|
restrictAge?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-allocated ID of the withdrawal group for the first withdrawal
|
||||||
|
* on this reserve.
|
||||||
|
*/
|
||||||
initialWithdrawalGroupId: string;
|
initialWithdrawalGroupId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -600,6 +610,8 @@ export interface PlanchetRecord {
|
|||||||
coinEv: CoinEnvelope;
|
coinEv: CoinEnvelope;
|
||||||
|
|
||||||
coinEvHash: string;
|
coinEvHash: string;
|
||||||
|
|
||||||
|
ageCommitmentProof?: AgeCommitmentProof;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -724,6 +736,8 @@ export interface CoinRecord {
|
|||||||
* Used to prevent allocation of the same coin for two different payments.
|
* Used to prevent allocation of the same coin for two different payments.
|
||||||
*/
|
*/
|
||||||
allocation?: CoinAllocation;
|
allocation?: CoinAllocation;
|
||||||
|
|
||||||
|
ageCommitmentProof?: AgeCommitmentProof;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CoinAllocation {
|
export interface CoinAllocation {
|
||||||
@ -1148,6 +1162,7 @@ export interface WalletContractData {
|
|||||||
wireMethod: string;
|
wireMethod: string;
|
||||||
wireInfoHash: string;
|
wireInfoHash: string;
|
||||||
maxDepositFee: AmountJson;
|
maxDepositFee: AmountJson;
|
||||||
|
minimumAge?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AbortStatus {
|
export enum AbortStatus {
|
||||||
|
@ -33,6 +33,7 @@ import {
|
|||||||
ExchangeSignKeyJson,
|
ExchangeSignKeyJson,
|
||||||
ExchangeWireJson,
|
ExchangeWireJson,
|
||||||
hashDenomPub,
|
hashDenomPub,
|
||||||
|
j2s,
|
||||||
LibtoolVersion,
|
LibtoolVersion,
|
||||||
Logger,
|
Logger,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
@ -445,6 +446,7 @@ async function downloadExchangeKeysInfo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
logger.info("received /keys response");
|
logger.info("received /keys response");
|
||||||
|
logger.info(`${j2s(exchangeKeysJsonUnchecked)}`);
|
||||||
|
|
||||||
if (exchangeKeysJsonUnchecked.denoms.length === 0) {
|
if (exchangeKeysJsonUnchecked.denoms.length === 0) {
|
||||||
throw TalerError.fromDetail(
|
throw TalerError.fromDetail(
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
|
AgeRestriction,
|
||||||
AmountJson,
|
AmountJson,
|
||||||
Amounts,
|
Amounts,
|
||||||
codecForContractTerms,
|
codecForContractTerms,
|
||||||
@ -197,6 +198,14 @@ export interface CoinSelectionRequest {
|
|||||||
maxWireFee: AmountJson;
|
maxWireFee: AmountJson;
|
||||||
|
|
||||||
maxDepositFee: AmountJson;
|
maxDepositFee: AmountJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum age requirement for the coin selection.
|
||||||
|
*
|
||||||
|
* When present, only select coins with either no age restriction
|
||||||
|
* or coins with an age commitment that matches the minimum age.
|
||||||
|
*/
|
||||||
|
minimumAge?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -651,6 +660,7 @@ export function extractContractData(
|
|||||||
merchant: parsedContractTerms.merchant,
|
merchant: parsedContractTerms.merchant,
|
||||||
products: parsedContractTerms.products,
|
products: parsedContractTerms.products,
|
||||||
summaryI18n: parsedContractTerms.summary_i18n,
|
summaryI18n: parsedContractTerms.summary_i18n,
|
||||||
|
minimumAge: parsedContractTerms.minimum_age,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -825,6 +835,8 @@ async function processDownloadProposalImpl(
|
|||||||
proposalResp.sig,
|
proposalResp.sig,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
logger.trace(`extracted contract data: ${j2s(contractData)}`);
|
||||||
|
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({ proposals: x.proposals, purchases: x.purchases }))
|
.mktx((x) => ({ proposals: x.proposals, purchases: x.purchases }))
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
@ -1379,6 +1391,11 @@ export async function generateDepositPermissions(
|
|||||||
const { coin, denom } = coinWithDenom[i];
|
const { coin, denom } = coinWithDenom[i];
|
||||||
let wireInfoHash: string;
|
let wireInfoHash: string;
|
||||||
wireInfoHash = contractData.wireInfoHash;
|
wireInfoHash = contractData.wireInfoHash;
|
||||||
|
logger.trace(
|
||||||
|
`signing deposit permission for coin with acp=${j2s(
|
||||||
|
coin.ageCommitmentProof,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
const dp = await ws.cryptoApi.signDepositPermission({
|
const dp = await ws.cryptoApi.signDepositPermission({
|
||||||
coinPriv: coin.coinPriv,
|
coinPriv: coin.coinPriv,
|
||||||
coinPub: coin.coinPub,
|
coinPub: coin.coinPub,
|
||||||
@ -1393,6 +1410,8 @@ export async function generateDepositPermissions(
|
|||||||
spendAmount: payCoinSel.coinContributions[i],
|
spendAmount: payCoinSel.coinContributions[i],
|
||||||
timestamp: contractData.timestamp,
|
timestamp: contractData.timestamp,
|
||||||
wireInfoHash,
|
wireInfoHash,
|
||||||
|
ageCommitmentProof: coin.ageCommitmentProof,
|
||||||
|
requiredMinimumAge: contractData.minimumAge,
|
||||||
});
|
});
|
||||||
depositPermissions.push(dp);
|
depositPermissions.push(dp);
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AgeCommitment,
|
||||||
|
AgeRestriction,
|
||||||
CoinPublicKeyString,
|
CoinPublicKeyString,
|
||||||
DenomKeyType,
|
DenomKeyType,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
@ -22,7 +24,9 @@ import {
|
|||||||
ExchangeProtocolVersion,
|
ExchangeProtocolVersion,
|
||||||
ExchangeRefreshRevealRequest,
|
ExchangeRefreshRevealRequest,
|
||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
|
HashCodeString,
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
|
j2s,
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
@ -83,6 +87,7 @@ import { GetReadWriteAccess } from "../util/query.js";
|
|||||||
import { guardOperationException } from "./common.js";
|
import { guardOperationException } from "./common.js";
|
||||||
import { CryptoApiStoppedError } from "../crypto/workers/cryptoDispatcher.js";
|
import { CryptoApiStoppedError } from "../crypto/workers/cryptoDispatcher.js";
|
||||||
import { TalerCryptoInterface } from "../crypto/cryptoImplementation.js";
|
import { TalerCryptoInterface } from "../crypto/cryptoImplementation.js";
|
||||||
|
import { TalerError } from "../errors.js";
|
||||||
|
|
||||||
const logger = new Logger("refresh.ts");
|
const logger = new Logger("refresh.ts");
|
||||||
|
|
||||||
@ -380,6 +385,7 @@ async function refreshMelt(
|
|||||||
meltCoinPriv: oldCoin.coinPriv,
|
meltCoinPriv: oldCoin.coinPriv,
|
||||||
meltCoinPub: oldCoin.coinPub,
|
meltCoinPub: oldCoin.coinPub,
|
||||||
feeRefresh: oldDenom.feeRefresh,
|
feeRefresh: oldDenom.feeRefresh,
|
||||||
|
meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
|
||||||
newCoinDenoms,
|
newCoinDenoms,
|
||||||
sessionSecretSeed: refreshSession.sessionSecretSeed,
|
sessionSecretSeed: refreshSession.sessionSecretSeed,
|
||||||
});
|
});
|
||||||
@ -388,6 +394,14 @@ async function refreshMelt(
|
|||||||
`coins/${oldCoin.coinPub}/melt`,
|
`coins/${oldCoin.coinPub}/melt`,
|
||||||
oldCoin.exchangeBaseUrl,
|
oldCoin.exchangeBaseUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let maybeAch: HashCodeString | undefined;
|
||||||
|
if (oldCoin.ageCommitmentProof) {
|
||||||
|
maybeAch = AgeRestriction.hashCommitment(
|
||||||
|
oldCoin.ageCommitmentProof.commitment,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const meltReqBody: ExchangeMeltRequest = {
|
const meltReqBody: ExchangeMeltRequest = {
|
||||||
coin_pub: oldCoin.coinPub,
|
coin_pub: oldCoin.coinPub,
|
||||||
confirm_sig: derived.confirmSig,
|
confirm_sig: derived.confirmSig,
|
||||||
@ -395,6 +409,7 @@ async function refreshMelt(
|
|||||||
denom_sig: oldCoin.denomSig,
|
denom_sig: oldCoin.denomSig,
|
||||||
rc: derived.hash,
|
rc: derived.hash,
|
||||||
value_with_fee: Amounts.stringify(derived.meltValueWithFee),
|
value_with_fee: Amounts.stringify(derived.meltValueWithFee),
|
||||||
|
age_commitment_hash: maybeAch,
|
||||||
};
|
};
|
||||||
|
|
||||||
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
|
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
|
||||||
@ -475,6 +490,7 @@ export async function assembleRefreshRevealRequest(args: {
|
|||||||
denomPubHash: string;
|
denomPubHash: string;
|
||||||
count: number;
|
count: number;
|
||||||
}[];
|
}[];
|
||||||
|
oldAgeCommitment?: AgeCommitment;
|
||||||
}): Promise<ExchangeRefreshRevealRequest> {
|
}): Promise<ExchangeRefreshRevealRequest> {
|
||||||
const {
|
const {
|
||||||
derived,
|
derived,
|
||||||
@ -517,6 +533,7 @@ export async function assembleRefreshRevealRequest(args: {
|
|||||||
transfer_privs: privs,
|
transfer_privs: privs,
|
||||||
transfer_pub: derived.transferPubs[norevealIndex],
|
transfer_pub: derived.transferPubs[norevealIndex],
|
||||||
link_sigs: linkSigs,
|
link_sigs: linkSigs,
|
||||||
|
old_age_commitment: args.oldAgeCommitment?.publicKeys,
|
||||||
};
|
};
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
@ -622,6 +639,7 @@ async function refreshReveal(
|
|||||||
meltCoinPub: oldCoin.coinPub,
|
meltCoinPub: oldCoin.coinPub,
|
||||||
feeRefresh: oldDenom.feeRefresh,
|
feeRefresh: oldDenom.feeRefresh,
|
||||||
newCoinDenoms,
|
newCoinDenoms,
|
||||||
|
meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
|
||||||
sessionSecretSeed: refreshSession.sessionSecretSeed,
|
sessionSecretSeed: refreshSession.sessionSecretSeed,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -637,6 +655,7 @@ async function refreshReveal(
|
|||||||
norevealIndex: norevealIndex,
|
norevealIndex: norevealIndex,
|
||||||
oldCoinPriv: oldCoin.coinPriv,
|
oldCoinPriv: oldCoin.coinPriv,
|
||||||
oldCoinPub: oldCoin.coinPub,
|
oldCoinPub: oldCoin.coinPub,
|
||||||
|
oldAgeCommitment: oldCoin.ageCommitmentProof?.commitment,
|
||||||
});
|
});
|
||||||
|
|
||||||
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
|
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
|
||||||
@ -822,6 +841,11 @@ async function processRefreshGroupImpl(
|
|||||||
logger.info(
|
logger.info(
|
||||||
"crypto API stopped while processing refresh group, probably the wallet is currently shutting down.",
|
"crypto API stopped while processing refresh group, probably the wallet is currently shutting down.",
|
||||||
);
|
);
|
||||||
|
} else if (x instanceof TalerError) {
|
||||||
|
logger.warn("process refresh session got exception (TalerError)");
|
||||||
|
logger.warn(`exc ${x}`);
|
||||||
|
logger.warn(`exc stack ${x.stack}`);
|
||||||
|
logger.warn(`error detail: ${j2s(x.errorDetail)}`);
|
||||||
} else {
|
} else {
|
||||||
logger.warn("process refresh session got exception");
|
logger.warn("process refresh session got exception");
|
||||||
logger.warn(`exc ${x}`);
|
logger.warn(`exc ${x}`);
|
||||||
|
@ -200,6 +200,7 @@ export async function createReserve(
|
|||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
currency: req.amount.currency,
|
currency: req.amount.currency,
|
||||||
operationStatus: OperationStatus.Pending,
|
operationStatus: OperationStatus.Pending,
|
||||||
|
restrictAge: req.restrictAge,
|
||||||
};
|
};
|
||||||
|
|
||||||
const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
|
const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
|
||||||
@ -541,12 +542,9 @@ async function updateReserve(
|
|||||||
const reserveUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl);
|
const reserveUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl);
|
||||||
reserveUrl.searchParams.set("timeout_ms", "200");
|
reserveUrl.searchParams.set("timeout_ms", "200");
|
||||||
|
|
||||||
const resp = await ws.http.get(
|
const resp = await ws.http.get(reserveUrl.href, {
|
||||||
reserveUrl.href,
|
timeout: getReserveRequestTimeout(reserve),
|
||||||
{
|
});
|
||||||
timeout: getReserveRequestTimeout(reserve),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await readSuccessResponseJsonOrErrorCode(
|
const result = await readSuccessResponseJsonOrErrorCode(
|
||||||
resp,
|
resp,
|
||||||
@ -632,17 +630,12 @@ async function updateReserve(
|
|||||||
amountReservePlus,
|
amountReservePlus,
|
||||||
amountReserveMinus,
|
amountReserveMinus,
|
||||||
).amount;
|
).amount;
|
||||||
const denomSel = selectWithdrawalDenominations(
|
const denomSel = selectWithdrawalDenominations(remainingAmount, denoms);
|
||||||
remainingAmount,
|
|
||||||
denoms,
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.trace(
|
logger.trace(
|
||||||
`Remaining unclaimed amount in reseve is ${Amounts.stringify(
|
`Remaining unclaimed amount in reseve is ${Amounts.stringify(
|
||||||
remainingAmount,
|
remainingAmount,
|
||||||
)} and can be withdrawn with ${
|
)} and can be withdrawn with ${denomSel.selectedDenoms.length} coins`,
|
||||||
denomSel.selectedDenoms.length
|
|
||||||
} coins`,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (denomSel.selectedDenoms.length === 0) {
|
if (denomSel.selectedDenoms.length === 0) {
|
||||||
@ -759,6 +752,7 @@ export async function createTalerWithdrawReserve(
|
|||||||
selectedExchange: string,
|
selectedExchange: string,
|
||||||
options: {
|
options: {
|
||||||
forcedDenomSel?: ForcedDenomSel;
|
forcedDenomSel?: ForcedDenomSel;
|
||||||
|
restrictAge?: number;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<AcceptWithdrawalResponse> {
|
): Promise<AcceptWithdrawalResponse> {
|
||||||
await updateExchangeFromUrl(ws, selectedExchange);
|
await updateExchangeFromUrl(ws, selectedExchange);
|
||||||
@ -774,6 +768,7 @@ export async function createTalerWithdrawReserve(
|
|||||||
exchange: selectedExchange,
|
exchange: selectedExchange,
|
||||||
senderWire: withdrawInfo.senderWire,
|
senderWire: withdrawInfo.senderWire,
|
||||||
exchangePaytoUri: exchangePaytoUri,
|
exchangePaytoUri: exchangePaytoUri,
|
||||||
|
restrictAge: options.restrictAge,
|
||||||
});
|
});
|
||||||
// We do this here, as the reserve should be registered before we return,
|
// We do this here, as the reserve should be registered before we return,
|
||||||
// so that we can redirect the user to the bank's status page.
|
// so that we can redirect the user to the bank's status page.
|
||||||
|
@ -32,6 +32,7 @@ test("withdrawal selection bug repro", (t) => {
|
|||||||
cipher: DenomKeyType.Rsa,
|
cipher: DenomKeyType.Rsa,
|
||||||
rsa_public_key:
|
rsa_public_key:
|
||||||
"040000XT67C8KBD6B75TTQ3SK8FWXMNQW4372T3BDDGPAMB9RFCA03638W8T3F71WFEFK9NP32VKYVNFXPYRWQ1N1HDKV5J0DFEKHBPJCYSWCBJDRNWD7G8BN8PT97FA9AMV75MYEK4X54D1HGJ207JSVJBGFCATSPNTEYNHEQF1F220W00TBZR1HNPDQFD56FG0DJQ9KGHM8EC33H6AY9YN9CNX5R3Z4TZ4Q23W47SBHB13H6W74FQJG1F50X38VRSC4SR8RWBAFB7S4K8D2H4NMRFSQT892A3T0BTBW7HM5C0H2CK6FRKG31F7W9WP1S29013K5CXYE55CT8TH6N8J9B780R42Y5S3ZB6J6E9H76XBPSGH4TGYSR2VZRB98J417KCQMZKX1BB67E7W5KVE37TC9SJ904002",
|
"040000XT67C8KBD6B75TTQ3SK8FWXMNQW4372T3BDDGPAMB9RFCA03638W8T3F71WFEFK9NP32VKYVNFXPYRWQ1N1HDKV5J0DFEKHBPJCYSWCBJDRNWD7G8BN8PT97FA9AMV75MYEK4X54D1HGJ207JSVJBGFCATSPNTEYNHEQF1F220W00TBZR1HNPDQFD56FG0DJQ9KGHM8EC33H6AY9YN9CNX5R3Z4TZ4Q23W47SBHB13H6W74FQJG1F50X38VRSC4SR8RWBAFB7S4K8D2H4NMRFSQT892A3T0BTBW7HM5C0H2CK6FRKG31F7W9WP1S29013K5CXYE55CT8TH6N8J9B780R42Y5S3ZB6J6E9H76XBPSGH4TGYSR2VZRB98J417KCQMZKX1BB67E7W5KVE37TC9SJ904002",
|
||||||
|
age_mask: 0,
|
||||||
},
|
},
|
||||||
denomPubHash:
|
denomPubHash:
|
||||||
"Q21FQSSG4FXNT96Z14CHXM8N1RZAG9GPHAV8PRWS0PZAAVWH7PBW6R97M2CH19KKP65NNSWXY7B6S53PT3CBM342E357ZXDDJ8RDVW8",
|
"Q21FQSSG4FXNT96Z14CHXM8N1RZAG9GPHAV8PRWS0PZAAVWH7PBW6R97M2CH19KKP65NNSWXY7B6S53PT3CBM342E357ZXDDJ8RDVW8",
|
||||||
@ -86,6 +87,7 @@ test("withdrawal selection bug repro", (t) => {
|
|||||||
cipher: DenomKeyType.Rsa,
|
cipher: DenomKeyType.Rsa,
|
||||||
rsa_public_key:
|
rsa_public_key:
|
||||||
"040000Y63CF78QFPKRY77BRK9P557Q1GQWX3NCZ3HSYSK0Z7TT0KGRA7N4SKBKEHSTVHX1Z9DNXMJR4EXSY1TXCKV0GJ3T3YYC6Z0JNMJFVYQAV4FX5J90NZH1N33MZTV8HS9SMNAA9S6K73G4P99GYBB01B0P6M1KXZ5JRDR7VWBR3MEJHHGJ6QBMCJR3NWJRE3WJW9PRY8QPQ2S7KFWTWRESH2DBXCXWBD2SRN6P9YX8GRAEMFEGXC9V5GVJTEMH6ZDGNXFPWZE3JVJ2Q4N9GDYKBCHZCJ7M7M2RJ9ZV4Y64NAN9BT6XDC68215GKKRHTW1BBF1MYY6AR3JCTT9HYAM923RMVQR3TAEB7SDX8J76XRZWYH3AGJCZAQGMN5C8SSH9AHQ9RNQJQ15CN45R37X4YNFJV904002",
|
"040000Y63CF78QFPKRY77BRK9P557Q1GQWX3NCZ3HSYSK0Z7TT0KGRA7N4SKBKEHSTVHX1Z9DNXMJR4EXSY1TXCKV0GJ3T3YYC6Z0JNMJFVYQAV4FX5J90NZH1N33MZTV8HS9SMNAA9S6K73G4P99GYBB01B0P6M1KXZ5JRDR7VWBR3MEJHHGJ6QBMCJR3NWJRE3WJW9PRY8QPQ2S7KFWTWRESH2DBXCXWBD2SRN6P9YX8GRAEMFEGXC9V5GVJTEMH6ZDGNXFPWZE3JVJ2Q4N9GDYKBCHZCJ7M7M2RJ9ZV4Y64NAN9BT6XDC68215GKKRHTW1BBF1MYY6AR3JCTT9HYAM923RMVQR3TAEB7SDX8J76XRZWYH3AGJCZAQGMN5C8SSH9AHQ9RNQJQ15CN45R37X4YNFJV904002",
|
||||||
|
age_mask: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
denomPubHash:
|
denomPubHash:
|
||||||
@ -141,6 +143,7 @@ test("withdrawal selection bug repro", (t) => {
|
|||||||
cipher: DenomKeyType.Rsa,
|
cipher: DenomKeyType.Rsa,
|
||||||
rsa_public_key:
|
rsa_public_key:
|
||||||
"040000YDESWC2B962DA4WK356SC50MA3N9KV0ZSGY3RC48JCTY258W909C7EEMT5BTC5KZ5T4CERCZ141P9QF87EK2BD1XEEM5GB07MB3H19WE4CQGAS8X84JBWN83PQGQXVMWE5HFA992KMGHC566GT9ZS2QPHZB6X89C4A80Z663PYAAPXP728VHAKATGNNBQ01ZZ2XD1CH9Y38YZBSPJ4K7GB2J76GBCYAVD9ENHDVWXJAXYRPBX4KSS5TXRR3K5NEN9ZV3AJD2V65K7ABRZDF5D5V1FJZZMNJ5XZ4FEREEKEBV9TDFPGJTKDEHEC60K3DN24DAATRESDJ1ZYYSYSRCAT4BT2B62ARGVMJTT5N2R126DRW9TGRWCW0ZAF2N2WET1H4NJEW77X0QT46Z5R3MZ0XPHD04002",
|
"040000YDESWC2B962DA4WK356SC50MA3N9KV0ZSGY3RC48JCTY258W909C7EEMT5BTC5KZ5T4CERCZ141P9QF87EK2BD1XEEM5GB07MB3H19WE4CQGAS8X84JBWN83PQGQXVMWE5HFA992KMGHC566GT9ZS2QPHZB6X89C4A80Z663PYAAPXP728VHAKATGNNBQ01ZZ2XD1CH9Y38YZBSPJ4K7GB2J76GBCYAVD9ENHDVWXJAXYRPBX4KSS5TXRR3K5NEN9ZV3AJD2V65K7ABRZDF5D5V1FJZZMNJ5XZ4FEREEKEBV9TDFPGJTKDEHEC60K3DN24DAATRESDJ1ZYYSYSRCAT4BT2B62ARGVMJTT5N2R126DRW9TGRWCW0ZAF2N2WET1H4NJEW77X0QT46Z5R3MZ0XPHD04002",
|
||||||
|
age_mask: 0,
|
||||||
},
|
},
|
||||||
denomPubHash:
|
denomPubHash:
|
||||||
"JS61DTKAFM0BX8Q4XV3ZSKB921SM8QK745Z2AFXTKFMBHHFNBD8TQ5ETJHFNDGBGX22FFN2A2ERNYG1SGSDQWNQHQQ2B14DBVJYJG8R",
|
"JS61DTKAFM0BX8Q4XV3ZSKB921SM8QK745Z2AFXTKFMBHHFNBD8TQ5ETJHFNDGBGX22FFN2A2ERNYG1SGSDQWNQHQQ2B14DBVJYJG8R",
|
||||||
@ -195,6 +198,7 @@ test("withdrawal selection bug repro", (t) => {
|
|||||||
cipher: DenomKeyType.Rsa,
|
cipher: DenomKeyType.Rsa,
|
||||||
rsa_public_key:
|
rsa_public_key:
|
||||||
"040000YG3T1ADB8DVA6BD3EPV6ZHSHTDW35DEN4VH1AE6CSB7P1PSDTNTJG866PHF6QB1CCWYCVRGA0FVBJ9Q0G7KV7AD9010GDYBQH0NNPHW744MTNXVXWBGGGRGQGYK4DTYN1DSWQ1FZNDSZZPB5BEKG2PDJ93NX2JTN06Y8QMS2G734Z9XHC10EENBG2KVB7EJ3CM8PV1T32RC7AY62F3496E8D8KRHJQQTT67DSGMNKK86QXVDTYW677FG27DP20E8XY3M6FQD53NDJ1WWES91401MV1A3VXVPGC76GZVDD62W3WTJ1YMKHTTA3MRXX3VEAAH3XTKDN1ER7X6CZPMYTF8VK735VP2B2TZGTF28TTW4FZS32SBS64APCDF6SZQ427N5538TJC7SRE71YSP5ET8GS904002",
|
"040000YG3T1ADB8DVA6BD3EPV6ZHSHTDW35DEN4VH1AE6CSB7P1PSDTNTJG866PHF6QB1CCWYCVRGA0FVBJ9Q0G7KV7AD9010GDYBQH0NNPHW744MTNXVXWBGGGRGQGYK4DTYN1DSWQ1FZNDSZZPB5BEKG2PDJ93NX2JTN06Y8QMS2G734Z9XHC10EENBG2KVB7EJ3CM8PV1T32RC7AY62F3496E8D8KRHJQQTT67DSGMNKK86QXVDTYW677FG27DP20E8XY3M6FQD53NDJ1WWES91401MV1A3VXVPGC76GZVDD62W3WTJ1YMKHTTA3MRXX3VEAAH3XTKDN1ER7X6CZPMYTF8VK735VP2B2TZGTF28TTW4FZS32SBS64APCDF6SZQ427N5538TJC7SRE71YSP5ET8GS904002",
|
||||||
|
age_mask: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
denomPubHash:
|
denomPubHash:
|
||||||
@ -250,6 +254,7 @@ test("withdrawal selection bug repro", (t) => {
|
|||||||
cipher: DenomKeyType.Rsa,
|
cipher: DenomKeyType.Rsa,
|
||||||
rsa_public_key:
|
rsa_public_key:
|
||||||
"040000ZC0G60E9QQ5PD81TSDWD9GV5Y6P8Z05NSPA696DP07NGQQVSRQXBA76Q6PRB0YFX295RG4MTQJXAZZ860ET307HSC2X37XAVGQXRVB8Q4F1V7NP5ZEVKTX75DZK1QRAVHEZGQYKSSH6DBCJNQF6V9WNQF3GEYVA4KCBHA7JF772KHXM9642C28Z0AS4XXXV2PABAN5C8CHYD5H7JDFNK3920W5Q69X0BS84XZ4RE2PW6HM1WZ6KGZ3MKWWWCPKQ1FSFABRBWKAB09PF563BEBXKY6M38QETPH5EDWGANHD0SC3QV0WXYVB7BNHNNQ0J5BNV56K563SYHM4E5ND260YRJSYA1GN5YSW2B1J5T1A1EBNYF2DN6JNJKWXWEQ42G5YS17ZSZ5EWDRA9QKV8EGTCNAD04002",
|
"040000ZC0G60E9QQ5PD81TSDWD9GV5Y6P8Z05NSPA696DP07NGQQVSRQXBA76Q6PRB0YFX295RG4MTQJXAZZ860ET307HSC2X37XAVGQXRVB8Q4F1V7NP5ZEVKTX75DZK1QRAVHEZGQYKSSH6DBCJNQF6V9WNQF3GEYVA4KCBHA7JF772KHXM9642C28Z0AS4XXXV2PABAN5C8CHYD5H7JDFNK3920W5Q69X0BS84XZ4RE2PW6HM1WZ6KGZ3MKWWWCPKQ1FSFABRBWKAB09PF563BEBXKY6M38QETPH5EDWGANHD0SC3QV0WXYVB7BNHNNQ0J5BNV56K563SYHM4E5ND260YRJSYA1GN5YSW2B1J5T1A1EBNYF2DN6JNJKWXWEQ42G5YS17ZSZ5EWDRA9QKV8EGTCNAD04002",
|
||||||
|
age_mask: 0,
|
||||||
},
|
},
|
||||||
denomPubHash:
|
denomPubHash:
|
||||||
"A41HW0Q2H9PCNMEWW0C0N45QAYVXZ8SBVRRAHE4W6X24SV1TH38ANTWDT80JXEBW9Z8PVPGT9GFV2EYZWJ5JW5W1N34NFNKHQSZ1PFR",
|
"A41HW0Q2H9PCNMEWW0C0N45QAYVXZ8SBVRRAHE4W6X24SV1TH38ANTWDT80JXEBW9Z8PVPGT9GFV2EYZWJ5JW5W1N34NFNKHQSZ1PFR",
|
||||||
@ -304,6 +309,7 @@ test("withdrawal selection bug repro", (t) => {
|
|||||||
cipher: DenomKeyType.Rsa,
|
cipher: DenomKeyType.Rsa,
|
||||||
rsa_public_key:
|
rsa_public_key:
|
||||||
"040000ZSK2PMVY6E3NBQ52KXMW029M60F4BWYTDS0FZSD0PE53CNZ9H6TM3GQK1WRTEKQ5GRWJ1J9DY6Y42SP47QVT1XD1G0W05SQ5F3F7P5KSWR0FJBJ9NZBXQEVN8Q4JRC94X3JJ3XV3KBYTZ2HTDFV28C3H2SRR0XGNZB4FY85NDZF1G4AEYJJ9QB3C0V8H70YB8RV3FKTNH7XS4K4HFNZHJ5H9VMX5SM9Z2DX37HA5WFH0E2MJBVVF2BWWA5M0HPPSB365RAE2AMD42Q65A96WD80X27SB2ZNQZ8WX0K13FWF85GZ6YNYAJGE1KGN06JDEKE9QD68Z651D7XE8V6664TVVC8M68S7WD0DSXMJQKQ0BNJXNDE29Q7MRX6DA3RW0PZ44B3TKRK0294FPVZTNSTA6XF04002",
|
"040000ZSK2PMVY6E3NBQ52KXMW029M60F4BWYTDS0FZSD0PE53CNZ9H6TM3GQK1WRTEKQ5GRWJ1J9DY6Y42SP47QVT1XD1G0W05SQ5F3F7P5KSWR0FJBJ9NZBXQEVN8Q4JRC94X3JJ3XV3KBYTZ2HTDFV28C3H2SRR0XGNZB4FY85NDZF1G4AEYJJ9QB3C0V8H70YB8RV3FKTNH7XS4K4HFNZHJ5H9VMX5SM9Z2DX37HA5WFH0E2MJBVVF2BWWA5M0HPPSB365RAE2AMD42Q65A96WD80X27SB2ZNQZ8WX0K13FWF85GZ6YNYAJGE1KGN06JDEKE9QD68Z651D7XE8V6664TVVC8M68S7WD0DSXMJQKQ0BNJXNDE29Q7MRX6DA3RW0PZ44B3TKRK0294FPVZTNSTA6XF04002",
|
||||||
|
age_mask: 0,
|
||||||
},
|
},
|
||||||
denomPubHash:
|
denomPubHash:
|
||||||
"F5NGBX33DTV4595XZZVK0S2MA1VMXFEJQERE5EBP5DS4QQ9EFRANN7YHWC1TKSHT2K6CQWDBRES8D3DWR0KZF5RET40B4AZXZ0RW1ZG",
|
"F5NGBX33DTV4595XZZVK0S2MA1VMXFEJQERE5EBP5DS4QQ9EFRANN7YHWC1TKSHT2K6CQWDBRES8D3DWR0KZF5RET40B4AZXZ0RW1ZG",
|
||||||
|
@ -266,8 +266,6 @@ export function selectForcedWithdrawalDenominations(
|
|||||||
denoms: DenominationRecord[],
|
denoms: DenominationRecord[],
|
||||||
forcedDenomSel: ForcedDenomSel,
|
forcedDenomSel: ForcedDenomSel,
|
||||||
): DenomSelectionState {
|
): DenomSelectionState {
|
||||||
let remaining = Amounts.copy(amountAvailable);
|
|
||||||
|
|
||||||
const selectedDenoms: {
|
const selectedDenoms: {
|
||||||
count: number;
|
count: number;
|
||||||
denomPubHash: string;
|
denomPubHash: string;
|
||||||
@ -454,6 +452,7 @@ async function processPlanchetGenerate(
|
|||||||
value: denom.value,
|
value: denom.value,
|
||||||
coinIndex: coinIdx,
|
coinIndex: coinIdx,
|
||||||
secretSeed: withdrawalGroup.secretSeed,
|
secretSeed: withdrawalGroup.secretSeed,
|
||||||
|
restrictAge: reserve.restrictAge,
|
||||||
});
|
});
|
||||||
const newPlanchet: PlanchetRecord = {
|
const newPlanchet: PlanchetRecord = {
|
||||||
blindingKey: r.blindingKey,
|
blindingKey: r.blindingKey,
|
||||||
@ -467,6 +466,7 @@ async function processPlanchetGenerate(
|
|||||||
withdrawalDone: false,
|
withdrawalDone: false,
|
||||||
withdrawSig: r.withdrawSig,
|
withdrawSig: r.withdrawSig,
|
||||||
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
|
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
|
||||||
|
ageCommitmentProof: r.ageCommitmentProof,
|
||||||
lastError: undefined,
|
lastError: undefined,
|
||||||
};
|
};
|
||||||
await ws.db
|
await ws.db
|
||||||
@ -701,6 +701,7 @@ async function processPlanchetVerifyAndStoreCoin(
|
|||||||
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
|
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
|
||||||
},
|
},
|
||||||
suspended: false,
|
suspended: false,
|
||||||
|
ageCommitmentProof: planchet.ageCommitmentProof,
|
||||||
};
|
};
|
||||||
|
|
||||||
const planchetCoinPub = planchet.coinPub;
|
const planchetCoinPub = planchet.coinPub;
|
||||||
@ -1101,11 +1102,6 @@ export async function getExchangeWithdrawalInfo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const withdrawFee = Amounts.sub(
|
|
||||||
selectedDenoms.totalWithdrawCost,
|
|
||||||
selectedDenoms.totalCoinValue,
|
|
||||||
).amount;
|
|
||||||
|
|
||||||
const ret: ExchangeWithdrawDetails = {
|
const ret: ExchangeWithdrawDetails = {
|
||||||
earliestDepositExpiration,
|
earliestDepositExpiration,
|
||||||
exchangeInfo: exchange,
|
exchangeInfo: exchange,
|
||||||
@ -1127,6 +1123,10 @@ export async function getExchangeWithdrawalInfo(
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetWithdrawalDetailsForUriOpts {
|
||||||
|
restrictAge?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get more information about a taler://withdraw URI.
|
* Get more information about a taler://withdraw URI.
|
||||||
*
|
*
|
||||||
@ -1137,6 +1137,7 @@ export async function getExchangeWithdrawalInfo(
|
|||||||
export async function getWithdrawalDetailsForUri(
|
export async function getWithdrawalDetailsForUri(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
talerWithdrawUri: string,
|
talerWithdrawUri: string,
|
||||||
|
opts: GetWithdrawalDetailsForUriOpts = {},
|
||||||
): Promise<WithdrawUriInfoResponse> {
|
): Promise<WithdrawUriInfoResponse> {
|
||||||
logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`);
|
logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`);
|
||||||
const info = await getBankWithdrawalInfo(ws.http, talerWithdrawUri);
|
const info = await getBankWithdrawalInfo(ws.http, talerWithdrawUri);
|
||||||
|
@ -36,6 +36,7 @@ function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo {
|
|||||||
denomPub: {
|
denomPub: {
|
||||||
cipher: DenomKeyType.Rsa,
|
cipher: DenomKeyType.Rsa,
|
||||||
rsa_public_key: "foobar",
|
rsa_public_key: "foobar",
|
||||||
|
age_mask: 0,
|
||||||
},
|
},
|
||||||
feeDeposit: a(feeDeposit),
|
feeDeposit: a(feeDeposit),
|
||||||
exchangeBaseUrl: "https://example.com/",
|
exchangeBaseUrl: "https://example.com/",
|
||||||
|
@ -843,6 +843,7 @@ async function dispatchRequestInternal(
|
|||||||
req.exchangeBaseUrl,
|
req.exchangeBaseUrl,
|
||||||
{
|
{
|
||||||
forcedDenomSel: req.forcedDenomSel,
|
forcedDenomSel: req.forcedDenomSel,
|
||||||
|
restrictAge: req.restrictAge,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1207,7 +1208,7 @@ class InternalWalletStateImpl implements InternalWalletState {
|
|||||||
) {
|
) {
|
||||||
this.cryptoDispatcher = new CryptoDispatcher(cryptoWorkerFactory);
|
this.cryptoDispatcher = new CryptoDispatcher(cryptoWorkerFactory);
|
||||||
this.cryptoApi = this.cryptoDispatcher.cryptoApi;
|
this.cryptoApi = this.cryptoDispatcher.cryptoApi;
|
||||||
this.timerGroup = new TimerGroup(timer)
|
this.timerGroup = new TimerGroup(timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDenomInfo(
|
async getDenomInfo(
|
||||||
|
Loading…
Reference in New Issue
Block a user