wallet: crypto worker fixes, better taler-crypto-worker integration

This commit is contained in:
Florian Dold 2022-03-24 01:10:34 +01:00
parent cc18751e72
commit 9d38cb56a6
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
7 changed files with 234 additions and 81 deletions

View File

@ -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,
@ -786,10 +755,10 @@ export function setupRefreshTransferPub(
}
/**
*
*
* @param paytoUri
* @param salt 16-byte salt
* @returns
* @param salt 16-byte salt
* @returns
*/
export function hashWire(paytoUri: string, salt: string): string {
const r = kdf(

View File

@ -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);
}
const resp = await wallet.ws.handleCoreApiRequest(
args.api.operation,
"reqid-1",
requestJson,
);
console.log(JSON.stringify(resp, undefined, 2));
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

View File

@ -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 {

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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}`);
}),
);
await Promise.all(ps);
logger.trace("refresh finished");
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(

View File

@ -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,