wallet: crypto worker fixes, better taler-crypto-worker integration
This commit is contained in:
parent
cc18751e72
commit
9d38cb56a6
@ -692,7 +692,7 @@ export interface FreshCoin {
|
||||
bks: Uint8Array;
|
||||
}
|
||||
|
||||
function bufferForUint32(n: number): Uint8Array {
|
||||
export function bufferForUint32(n: number): Uint8Array {
|
||||
const arrBuf = new ArrayBuffer(4);
|
||||
const buf = new Uint8Array(arrBuf);
|
||||
const dv = new DataView(arrBuf);
|
||||
@ -700,37 +700,6 @@ function bufferForUint32(n: number): Uint8Array {
|
||||
return buf;
|
||||
}
|
||||
|
||||
export function setupRefreshPlanchet(
|
||||
transferSecret: Uint8Array,
|
||||
coinNumber: number,
|
||||
): FreshCoin {
|
||||
// See TALER_transfer_secret_to_planchet_secret in C impl
|
||||
const planchetMasterSecret = kdfKw({
|
||||
ikm: transferSecret,
|
||||
outputLength: 32,
|
||||
salt: bufferForUint32(coinNumber),
|
||||
info: stringToBytes("taler-coin-derivation"),
|
||||
});
|
||||
|
||||
const coinPriv = kdfKw({
|
||||
ikm: planchetMasterSecret,
|
||||
outputLength: 32,
|
||||
salt: stringToBytes("coin"),
|
||||
});
|
||||
|
||||
const bks = kdfKw({
|
||||
ikm: planchetMasterSecret,
|
||||
outputLength: 32,
|
||||
salt: stringToBytes("bks"),
|
||||
});
|
||||
|
||||
return {
|
||||
bks,
|
||||
coinPriv,
|
||||
coinPub: eddsaGetPublic(coinPriv),
|
||||
};
|
||||
}
|
||||
|
||||
export function setupWithdrawPlanchet(
|
||||
secretSeed: Uint8Array,
|
||||
coinNumber: number,
|
||||
|
@ -64,7 +64,10 @@ import { runBench1 } from "./bench1.js";
|
||||
import { runEnv1 } from "./env1.js";
|
||||
import { GlobalTestState, runTestWithState } from "./harness/harness.js";
|
||||
import { runBench2 } from "./bench2.js";
|
||||
import { TalerCryptoInterface, TalerCryptoInterfaceR } from "@gnu-taler/taler-wallet-core/src/crypto/cryptoImplementation";
|
||||
import {
|
||||
TalerCryptoInterface,
|
||||
TalerCryptoInterfaceR,
|
||||
} from "@gnu-taler/taler-wallet-core/src/crypto/cryptoImplementation";
|
||||
|
||||
// This module also serves as the entry point for the crypto
|
||||
// thread worker, and thus must expose these two handlers.
|
||||
@ -75,6 +78,12 @@ export {
|
||||
|
||||
const logger = new Logger("taler-wallet-cli.ts");
|
||||
|
||||
process.on("unhandledRejection", (error: any) => {
|
||||
logger.error("unhandledRejection", error.message);
|
||||
logger.error("stack", error.stack);
|
||||
process.exit(2);
|
||||
});
|
||||
|
||||
const defaultWalletDbPath = os.homedir + "/" + ".talerwalletdb.json";
|
||||
|
||||
function assertUnreachable(x: never): never {
|
||||
@ -218,6 +227,7 @@ async function withWallet<T>(
|
||||
} finally {
|
||||
logger.info("operation with wallet finished, stopping");
|
||||
wallet.stop();
|
||||
logger.info("stopped wallet");
|
||||
}
|
||||
}
|
||||
|
||||
@ -250,13 +260,18 @@ walletCli
|
||||
console.error("Invalid JSON");
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
const resp = await wallet.ws.handleCoreApiRequest(
|
||||
args.api.operation,
|
||||
"reqid-1",
|
||||
requestJson,
|
||||
);
|
||||
console.log(JSON.stringify(resp, undefined, 2));
|
||||
} catch (e) {
|
||||
logger.error(`Got exception while handling API request ${e}`);
|
||||
}
|
||||
});
|
||||
logger.info("finished handling API request");
|
||||
});
|
||||
|
||||
walletCli
|
||||
|
@ -42,7 +42,6 @@ import {
|
||||
eddsaVerify,
|
||||
encodeCrock,
|
||||
ExchangeProtocolVersion,
|
||||
FreshCoin,
|
||||
hash,
|
||||
hashCoinEv,
|
||||
hashCoinEvInner,
|
||||
@ -53,14 +52,12 @@ import {
|
||||
MakeSyncSignatureRequest,
|
||||
PlanchetCreationRequest,
|
||||
WithdrawalPlanchet,
|
||||
randomBytes,
|
||||
RecoupRefreshRequest,
|
||||
RecoupRequest,
|
||||
RefreshPlanchetInfo,
|
||||
rsaBlind,
|
||||
rsaUnblind,
|
||||
rsaVerify,
|
||||
setupRefreshPlanchet,
|
||||
setupRefreshTransferPub,
|
||||
setupTipPlanchet,
|
||||
setupWithdrawPlanchet,
|
||||
@ -70,6 +67,8 @@ import {
|
||||
UnblindedSignature,
|
||||
PlanchetUnblindInfo,
|
||||
TalerProtocolTimestamp,
|
||||
kdfKw,
|
||||
bufferForUint32,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import bigint from "big-integer";
|
||||
import { DenominationRecord, WireFee } from "../db.js";
|
||||
@ -141,6 +140,8 @@ export interface TalerCryptoInterface {
|
||||
|
||||
rsaVerify(req: RsaVerificationRequest): Promise<ValidationResult>;
|
||||
|
||||
rsaBlind(req: RsaBlindRequest): Promise<RsaBlindResponse>;
|
||||
|
||||
signDepositPermission(
|
||||
depositInfo: DepositInfo,
|
||||
): Promise<CoinDepositPermission>;
|
||||
@ -154,6 +155,14 @@ export interface TalerCryptoInterface {
|
||||
signCoinLink(req: SignCoinLinkRequest): Promise<EddsaSigningResult>;
|
||||
|
||||
makeSyncSignature(req: MakeSyncSignatureRequest): Promise<EddsaSigningResult>;
|
||||
|
||||
setupRefreshPlanchet(
|
||||
req: SetupRefreshPlanchetRequest,
|
||||
): Promise<FreshCoinEncoded>;
|
||||
|
||||
keyExchangeEcdheEddsa(
|
||||
req: KeyExchangeEcdheEddsaRequest,
|
||||
): Promise<KeyExchangeResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -257,6 +266,19 @@ export const nullCrypto: TalerCryptoInterface = {
|
||||
): Promise<EddsaSigningResult> {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
setupRefreshPlanchet: function (
|
||||
req: SetupRefreshPlanchetRequest,
|
||||
): Promise<FreshCoinEncoded> {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
rsaBlind: function (req: RsaBlindRequest): Promise<RsaBlindResponse> {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
keyExchangeEcdheEddsa: function (
|
||||
req: KeyExchangeEcdheEddsaRequest,
|
||||
): Promise<KeyExchangeResult> {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
};
|
||||
|
||||
export type WithArg<X> = X extends (req: infer T) => infer R
|
||||
@ -275,12 +297,23 @@ export interface SignCoinLinkRequest {
|
||||
coinEv: CoinEnvelope;
|
||||
}
|
||||
|
||||
export interface SetupRefreshPlanchetRequest {
|
||||
transferSecret: string;
|
||||
coinNumber: number;
|
||||
}
|
||||
|
||||
export interface RsaVerificationRequest {
|
||||
hm: string;
|
||||
sig: string;
|
||||
pk: string;
|
||||
}
|
||||
|
||||
export interface RsaBlindRequest {
|
||||
hm: string;
|
||||
bks: string;
|
||||
pub: string;
|
||||
}
|
||||
|
||||
export interface EddsaSigningResult {
|
||||
sig: string;
|
||||
}
|
||||
@ -341,16 +374,35 @@ export interface UnblindDenominationSignatureRequest {
|
||||
evSig: BlindedDenominationSignature;
|
||||
}
|
||||
|
||||
export interface FreshCoinEncoded {
|
||||
coinPub: string;
|
||||
coinPriv: string;
|
||||
bks: string;
|
||||
}
|
||||
|
||||
export interface RsaUnblindRequest {
|
||||
blindedSig: string;
|
||||
bk: string;
|
||||
pk: string;
|
||||
}
|
||||
|
||||
export interface RsaBlindResponse {
|
||||
blinded: string;
|
||||
}
|
||||
|
||||
export interface RsaUnblindResponse {
|
||||
sig: string;
|
||||
}
|
||||
|
||||
export interface KeyExchangeEcdheEddsaRequest {
|
||||
ecdhePriv: string;
|
||||
eddsaPub: string;
|
||||
}
|
||||
|
||||
export interface KeyExchangeResult {
|
||||
h: string;
|
||||
}
|
||||
|
||||
export const nativeCryptoR: TalerCryptoInterfaceR = {
|
||||
async eddsaSign(
|
||||
tci: TalerCryptoInterfaceR,
|
||||
@ -361,6 +413,53 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
||||
};
|
||||
},
|
||||
|
||||
async rsaBlind(
|
||||
tci: TalerCryptoInterfaceR,
|
||||
req: RsaBlindRequest,
|
||||
): Promise<RsaBlindResponse> {
|
||||
const res = rsaBlind(
|
||||
decodeCrock(req.hm),
|
||||
decodeCrock(req.bks),
|
||||
decodeCrock(req.pub),
|
||||
);
|
||||
return {
|
||||
blinded: encodeCrock(res),
|
||||
};
|
||||
},
|
||||
|
||||
async setupRefreshPlanchet(
|
||||
tci: TalerCryptoInterfaceR,
|
||||
req: SetupRefreshPlanchetRequest,
|
||||
): Promise<FreshCoinEncoded> {
|
||||
const transferSecret = decodeCrock(req.transferSecret);
|
||||
const coinNumber = req.coinNumber;
|
||||
// See TALER_transfer_secret_to_planchet_secret in C impl
|
||||
const planchetMasterSecret = kdfKw({
|
||||
ikm: transferSecret,
|
||||
outputLength: 32,
|
||||
salt: bufferForUint32(coinNumber),
|
||||
info: stringToBytes("taler-coin-derivation"),
|
||||
});
|
||||
|
||||
const coinPriv = kdfKw({
|
||||
ikm: planchetMasterSecret,
|
||||
outputLength: 32,
|
||||
salt: stringToBytes("coin"),
|
||||
});
|
||||
|
||||
const bks = kdfKw({
|
||||
ikm: planchetMasterSecret,
|
||||
outputLength: 32,
|
||||
salt: stringToBytes("bks"),
|
||||
});
|
||||
|
||||
return {
|
||||
bks: encodeCrock(bks),
|
||||
coinPriv: encodeCrock(coinPriv),
|
||||
coinPub: encodeCrock(eddsaGetPublic(coinPriv)),
|
||||
};
|
||||
},
|
||||
|
||||
async createPlanchet(
|
||||
tci: TalerCryptoInterfaceR,
|
||||
req: PlanchetCreationRequest,
|
||||
@ -374,10 +473,14 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
||||
req.coinIndex,
|
||||
);
|
||||
const coinPubHash = hash(derivedPlanchet.coinPub);
|
||||
const ev = rsaBlind(coinPubHash, derivedPlanchet.bks, denomPubRsa);
|
||||
const blindResp = await tci.rsaBlind(tci, {
|
||||
bks: encodeCrock(derivedPlanchet.bks),
|
||||
hm: encodeCrock(coinPubHash),
|
||||
pub: denomPub.rsa_public_key,
|
||||
});
|
||||
const coinEv: CoinEnvelope = {
|
||||
cipher: DenomKeyType.Rsa,
|
||||
rsa_blinded_planchet: encodeCrock(ev),
|
||||
rsa_blinded_planchet: blindResp.blinded,
|
||||
};
|
||||
const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount;
|
||||
const denomPubHash = hashDenomPub(req.denomPub);
|
||||
@ -423,10 +526,14 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
||||
const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex);
|
||||
const denomPub = decodeCrock(req.denomPub.rsa_public_key);
|
||||
const coinPubHash = hash(fc.coinPub);
|
||||
const ev = rsaBlind(coinPubHash, fc.bks, denomPub);
|
||||
const blindResp = await tci.rsaBlind(tci, {
|
||||
bks: encodeCrock(fc.bks),
|
||||
hm: encodeCrock(coinPubHash),
|
||||
pub: encodeCrock(denomPub),
|
||||
});
|
||||
const coinEv = {
|
||||
cipher: DenomKeyType.Rsa,
|
||||
rsa_blinded_planchet: encodeCrock(ev),
|
||||
rsa_blinded_planchet: blindResp.blinded,
|
||||
};
|
||||
const tipPlanchet: DerivedTipPlanchet = {
|
||||
blindingKey: encodeCrock(fc.bks),
|
||||
@ -798,32 +905,32 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
||||
const denomSel = newCoinDenoms[j];
|
||||
for (let k = 0; k < denomSel.count; k++) {
|
||||
const coinIndex = planchets.length;
|
||||
const transferPriv = decodeCrock(transferPrivs[i]);
|
||||
const oldCoinPub = decodeCrock(meltCoinPub);
|
||||
const transferSecret = keyExchangeEcdheEddsa(
|
||||
transferPriv,
|
||||
oldCoinPub,
|
||||
);
|
||||
const transferSecretRes = await tci.keyExchangeEcdheEddsa(tci, {
|
||||
ecdhePriv: transferPrivs[i],
|
||||
eddsaPub: meltCoinPub,
|
||||
});
|
||||
let coinPub: Uint8Array;
|
||||
let coinPriv: Uint8Array;
|
||||
let blindingFactor: Uint8Array;
|
||||
// FIXME: make setupRefreshPlanchet a crypto api fn
|
||||
let fresh: FreshCoin = setupRefreshPlanchet(
|
||||
transferSecret,
|
||||
coinIndex,
|
||||
);
|
||||
coinPriv = fresh.coinPriv;
|
||||
coinPub = fresh.coinPub;
|
||||
blindingFactor = fresh.bks;
|
||||
let fresh: FreshCoinEncoded = await tci.setupRefreshPlanchet(tci, {
|
||||
coinNumber: coinIndex,
|
||||
transferSecret: transferSecretRes.h,
|
||||
});
|
||||
coinPriv = decodeCrock(fresh.coinPriv);
|
||||
coinPub = decodeCrock(fresh.coinPub);
|
||||
blindingFactor = decodeCrock(fresh.bks);
|
||||
const coinPubHash = hash(coinPub);
|
||||
if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) {
|
||||
throw Error("unsupported cipher, can't create refresh session");
|
||||
}
|
||||
const rsaDenomPub = decodeCrock(denomSel.denomPub.rsa_public_key);
|
||||
const ev = rsaBlind(coinPubHash, blindingFactor, rsaDenomPub);
|
||||
const blindResult = await tci.rsaBlind(tci, {
|
||||
bks: encodeCrock(blindingFactor),
|
||||
hm: encodeCrock(coinPubHash),
|
||||
pub: denomSel.denomPub.rsa_public_key,
|
||||
});
|
||||
const coinEv: CoinEnvelope = {
|
||||
cipher: DenomKeyType.Rsa,
|
||||
rsa_blinded_planchet: encodeCrock(ev),
|
||||
rsa_blinded_planchet: blindResult.blinded,
|
||||
};
|
||||
const coinEvHash = hashCoinEv(
|
||||
coinEv,
|
||||
@ -921,6 +1028,19 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
||||
const uploadSig = eddsaSign(sigBlob, decodeCrock(req.accountPriv));
|
||||
return { sig: encodeCrock(uploadSig) };
|
||||
},
|
||||
async keyExchangeEcdheEddsa(
|
||||
tci: TalerCryptoInterfaceR,
|
||||
req: KeyExchangeEcdheEddsaRequest,
|
||||
): Promise<KeyExchangeResult> {
|
||||
return {
|
||||
h: encodeCrock(
|
||||
keyExchangeEcdheEddsa(
|
||||
decodeCrock(req.ecdhePriv),
|
||||
decodeCrock(req.eddsaPub),
|
||||
),
|
||||
),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function amountToBuffer(amount: AmountJson): Uint8Array {
|
||||
|
@ -122,7 +122,7 @@ export class CryptoDispatcher {
|
||||
worker.idleTimeoutHandle = null;
|
||||
}
|
||||
if (worker.currentWorkItem) {
|
||||
worker.currentWorkItem.reject(Error("explicitly terminated"));
|
||||
worker.currentWorkItem.reject(new CryptoApiStoppedError());
|
||||
worker.currentWorkItem = null;
|
||||
}
|
||||
if (worker.w) {
|
||||
@ -143,7 +143,7 @@ export class CryptoDispatcher {
|
||||
*/
|
||||
wake(ws: WorkerState, work: WorkItem): void {
|
||||
if (this.stopped) {
|
||||
throw new CryptoApiStoppedError();
|
||||
return;
|
||||
}
|
||||
if (ws.currentWorkItem !== null) {
|
||||
throw Error("assertion failed");
|
||||
@ -331,8 +331,8 @@ export class CryptoDispatcher {
|
||||
}
|
||||
timeout.clear();
|
||||
resolve(x);
|
||||
});
|
||||
p.catch((x) => {
|
||||
}).catch((x) => {
|
||||
logger.info(`crypto RPC call ${operation} threw`);
|
||||
if (timedOut) {
|
||||
return;
|
||||
}
|
||||
|
@ -47,14 +47,11 @@ export class SynchronousCryptoWorker {
|
||||
|
||||
this.cryptoImplR = { ...nativeCryptoR };
|
||||
|
||||
if (
|
||||
process.env["TALER_WALLET_RPC_CRYPRO"] ||
|
||||
// Old name
|
||||
process.env["TALER_WALLET_PRIMITIVE_WORKER"]
|
||||
) {
|
||||
if (process.env["TALER_WALLET_PRIMITIVE_WORKER"]) {
|
||||
logger.info("using RPC for some crypto operations");
|
||||
const rpc = (this.rpcClient = new CryptoRpcClient());
|
||||
this.cryptoImplR.eddsaSign = async (_, req) => {
|
||||
logger.trace("making RPC request");
|
||||
logger.info("calling RPC impl of eddsaSign");
|
||||
return await rpc.queueRequest({
|
||||
op: "eddsa_sign",
|
||||
args: {
|
||||
@ -63,6 +60,46 @@ export class SynchronousCryptoWorker {
|
||||
},
|
||||
});
|
||||
};
|
||||
this.cryptoImplR.setupRefreshPlanchet = async (_, req) => {
|
||||
const res = await rpc.queueRequest({
|
||||
op: "setup_refresh_planchet",
|
||||
args: {
|
||||
coin_index: req.coinNumber,
|
||||
transfer_secret: req.transferSecret,
|
||||
},
|
||||
});
|
||||
return {
|
||||
bks: res.blinding_key,
|
||||
coinPriv: res.coin_priv,
|
||||
coinPub: res.coin_pub,
|
||||
};
|
||||
};
|
||||
this.cryptoImplR.rsaBlind = async (_, req) => {
|
||||
const res = await rpc.queueRequest({
|
||||
op: "rsa_blind",
|
||||
args: {
|
||||
bks: req.bks,
|
||||
hm: req.hm,
|
||||
pub: req.pub,
|
||||
},
|
||||
});
|
||||
return {
|
||||
blinded: res.blinded,
|
||||
};
|
||||
};
|
||||
|
||||
this.cryptoImplR.keyExchangeEcdheEddsa = async (_, req) => {
|
||||
const res = await rpc.queueRequest({
|
||||
op: "kx_ecdhe_eddsa",
|
||||
args: {
|
||||
ecdhe_priv: req.ecdhePriv,
|
||||
eddsa_pub: req.eddsaPub,
|
||||
},
|
||||
});
|
||||
return {
|
||||
h: res.h,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,8 +138,8 @@ export class SynchronousCryptoWorker {
|
||||
let result: any;
|
||||
try {
|
||||
result = await (impl as any)[operation](impl, req);
|
||||
} catch (e) {
|
||||
logger.error("error during operation", e);
|
||||
} catch (e: any) {
|
||||
logger.error(`error during operation '${operation}': ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -797,11 +797,22 @@ async function processRefreshGroupImpl(
|
||||
return;
|
||||
}
|
||||
// Process refresh sessions of the group in parallel.
|
||||
logger.trace("processing refresh sessions for old coins");
|
||||
const ps = refreshGroup.oldCoinPubs.map((x, i) =>
|
||||
processRefreshSession(ws, refreshGroupId, i),
|
||||
processRefreshSession(ws, refreshGroupId, i).catch((x) => {
|
||||
logger.warn("process refresh session got exception");
|
||||
logger.warn(`exc ${x}`);
|
||||
logger.warn(`exc stack ${x.stack}`);
|
||||
}),
|
||||
);
|
||||
try {
|
||||
logger.trace("waiting for refreshes");
|
||||
await Promise.all(ps);
|
||||
logger.trace("refresh finished");
|
||||
} catch (e) {
|
||||
logger.warn("process refresh sessions got exception");
|
||||
logger.warn(`exception: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function processRefreshSession(
|
||||
|
@ -1077,6 +1077,7 @@ export async function handleCoreApiRequest(
|
||||
};
|
||||
} catch (e: any) {
|
||||
const err = getErrorDetailFromException(e);
|
||||
logger.info(`finished wallet core request with error: ${j2s(err)}`);
|
||||
return {
|
||||
type: "error",
|
||||
operation,
|
||||
|
Loading…
Reference in New Issue
Block a user