wallet: simplify crypto workers

This commit is contained in:
Florian Dold 2022-03-23 21:24:23 +01:00
parent e21c1b3192
commit d881f4fd25
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
27 changed files with 789 additions and 719 deletions

View File

@ -27,7 +27,7 @@ import {
import {
checkReserve,
createFakebankReserve,
CryptoApi,
CryptoDispatcher,
depositCoin,
downloadExchangeInfo,
findDenomOrThrow,
@ -50,7 +50,8 @@ export async function runBench2(configJson: any): Promise<void> {
// Validate the configuration file for this benchmark.
const benchConf = codecForBench2Config().decode(configJson);
const curr = benchConf.currency;
const cryptoApi = new CryptoApi(new SynchronousCryptoWorkerFactory());
const cryptoDisp = new CryptoDispatcher(new SynchronousCryptoWorkerFactory());
const cryptoApi = cryptoDisp.cryptoApi;
const http = new NodeHttpLib();
http.setThrottling(false);

View File

@ -50,18 +50,21 @@ import {
NodeHttpLib,
getDefaultNodeWallet,
NodeThreadCryptoWorkerFactory,
CryptoApi,
walletCoreDebugFlags,
WalletApiOperation,
WalletCoreApiClient,
Wallet,
getErrorDetailFromException,
CryptoDispatcher,
SynchronousCryptoWorkerFactory,
nativeCrypto,
} from "@gnu-taler/taler-wallet-core";
import { lintExchangeDeployment } from "./lint.js";
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";
// This module also serves as the entry point for the crypto
// thread worker, and thus must expose these two handlers.
@ -1121,14 +1124,30 @@ testCli.subcommand("tvgcheck", "tvgcheck").action(async (args) => {
console.log("check passed!");
});
testCli.subcommand("cryptoworker", "cryptoworker").action(async (args) => {
const workerFactory = new NodeThreadCryptoWorkerFactory();
const cryptoApi = new CryptoApi(workerFactory);
const input = "foo";
console.log(`testing crypto worker by hashing string '${input}'`);
const res = await cryptoApi.hashString(input);
console.log(res);
});
testCli
.subcommand("cryptoworker", "cryptoworker")
.maybeOption("impl", ["--impl"], clk.STRING)
.action(async (args) => {
let cryptoApi: TalerCryptoInterface;
if (!args.cryptoworker.impl || args.cryptoworker.impl === "node") {
const workerFactory = new NodeThreadCryptoWorkerFactory();
const cryptoDisp = new CryptoDispatcher(workerFactory);
cryptoApi = cryptoDisp.cryptoApi;
} else if (args.cryptoworker.impl === "sync") {
const workerFactory = new SynchronousCryptoWorkerFactory();
const cryptoDisp = new CryptoDispatcher(workerFactory);
cryptoApi = cryptoDisp.cryptoApi;
} else if (args.cryptoworker.impl === "none") {
cryptoApi = nativeCrypto;
} else {
throw Error("invalid impl");
}
const input = "foo";
console.log(`testing crypto worker by hashing string '${input}'`);
const res = await cryptoApi.hashString({ str: input });
console.log(res);
});
export function main() {
if (process.env["TALER_WALLET_DEBUG_DENOMSEL_ALLOW_LATE"]) {

View File

@ -20,7 +20,7 @@
import { j2s } from "@gnu-taler/taler-util";
import {
checkReserve,
CryptoApi,
CryptoDispatcher,
depositCoin,
downloadExchangeInfo,
findDenomOrThrow,
@ -44,7 +44,8 @@ export async function runWalletDblessTest(t: GlobalTestState) {
const { bank, exchange } = await createSimpleTestkudosEnvironment(t);
const http = new NodeHttpLib();
const cryptoApi = new CryptoApi(new SynchronousCryptoWorkerFactory());
const cryptiDisp = new CryptoDispatcher(new SynchronousCryptoWorkerFactory());
const cryptoApi = cryptiDisp.cryptoApi;
try {
// Withdraw digital cash into the wallet.

View File

@ -44,7 +44,6 @@ import {
ExchangeProtocolVersion,
FreshCoin,
hash,
HashCodeString,
hashCoinEv,
hashCoinEvInner,
hashDenomPub,
@ -67,15 +66,13 @@ import {
setupWithdrawPlanchet,
stringToBytes,
TalerSignaturePurpose,
AbsoluteTime,
BlindedDenominationSignature,
UnblindedSignature,
PlanchetUnblindInfo,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
import bigint from "big-integer";
import { DenominationRecord, WireFee } from "../../db.js";
import * as timer from "../../util/timer.js";
import { DenominationRecord, WireFee } from "../db.js";
import {
CreateRecoupRefreshReqRequest,
CreateRecoupReqRequest,
@ -84,90 +81,288 @@ import {
DeriveRefreshSessionRequest,
DeriveTipRequest,
SignTrackTransactionRequest,
} from "../cryptoTypes.js";
} from "./cryptoTypes.js";
const logger = new Logger("cryptoImplementation.ts");
function amountToBuffer(amount: AmountJson): Uint8Array {
const buffer = new ArrayBuffer(8 + 4 + 12);
const dvbuf = new DataView(buffer);
const u8buf = new Uint8Array(buffer);
const curr = stringToBytes(amount.currency);
if (typeof dvbuf.setBigUint64 !== "undefined") {
dvbuf.setBigUint64(0, BigInt(amount.value));
} else {
const arr = bigint(amount.value).toArray(2 ** 8).value;
let offset = 8 - arr.length;
for (let i = 0; i < arr.length; i++) {
dvbuf.setUint8(offset++, arr[i]);
}
}
dvbuf.setUint32(8, amount.fraction);
u8buf.set(curr, 8 + 4);
return u8buf;
}
function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array {
const b = new ArrayBuffer(8);
const v = new DataView(b);
// The buffer we sign over represents the timestamp in microseconds.
if (typeof v.setBigUint64 !== "undefined") {
const s = BigInt(ts.t_s) * BigInt(1000 * 1000);
v.setBigUint64(0, s);
} else {
const s =
ts.t_s === "never" ? bigint.zero : bigint(ts.t_s).multiply(1000 * 1000);
const arr = s.toArray(2 ** 8).value;
let offset = 8 - arr.length;
for (let i = 0; i < arr.length; i++) {
v.setUint8(offset++, arr[i]);
}
}
return new Uint8Array(b);
}
export interface PrimitiveWorker {
setupRefreshPlanchet(arg0: {
transfer_secret: string;
coin_index: number;
}): Promise<{
coin_pub: string;
coin_priv: string;
blinding_key: string;
}>;
eddsaVerify(req: {
msg: string;
sig: string;
pub: string;
}): Promise<{ valid: boolean }>;
eddsaSign(req: { msg: string; priv: string }): Promise<{ sig: string }>;
}
async function myEddsaSign(
primitiveWorker: PrimitiveWorker | undefined,
req: { msg: string; priv: string },
): Promise<{ sig: string }> {
if (primitiveWorker) {
return primitiveWorker.eddsaSign(req);
}
const sig = eddsaSign(decodeCrock(req.msg), decodeCrock(req.priv));
return {
sig: encodeCrock(sig),
};
}
export class CryptoImplementation {
static enableTracing = false;
constructor(private primitiveWorker?: PrimitiveWorker) {}
//const logger = new Logger("cryptoImplementation.ts");
/**
* Interface for (asynchronous) cryptographic operations that
* Taler uses.
*/
export interface TalerCryptoInterface {
/**
* Create a pre-coin of the given denomination to be withdrawn from then given
* reserve.
*/
createPlanchet(req: PlanchetCreationRequest): Promise<WithdrawalPlanchet>;
eddsaSign(req: EddsaSignRequest): Promise<EddsaSignResponse>;
/**
* Create a planchet used for tipping, including the private keys.
*/
createTipPlanchet(req: DeriveTipRequest): Promise<DerivedTipPlanchet>;
signTrackTransaction(
req: SignTrackTransactionRequest,
): Promise<EddsaSigningResult>;
createRecoupRequest(req: CreateRecoupReqRequest): Promise<RecoupRequest>;
createRecoupRefreshRequest(
req: CreateRecoupRefreshReqRequest,
): Promise<RecoupRefreshRequest>;
isValidPaymentSignature(
req: PaymentSignatureValidationRequest,
): Promise<ValidationResult>;
isValidWireFee(req: WireFeeValidationRequest): Promise<ValidationResult>;
isValidDenom(req: DenominationValidationRequest): Promise<ValidationResult>;
isValidWireAccount(
req: WireAccountValidationRequest,
): Promise<ValidationResult>;
isValidContractTermsSignature(
req: ContractTermsValidationRequest,
): Promise<ValidationResult>;
createEddsaKeypair(req: {}): Promise<EddsaKeypair>;
eddsaGetPublic(req: EddsaGetPublicRequest): Promise<EddsaKeypair>;
unblindDenominationSignature(
req: UnblindDenominationSignatureRequest,
): Promise<UnblindedSignature>;
rsaUnblind(req: RsaUnblindRequest): Promise<RsaUnblindResponse>;
rsaVerify(req: RsaVerificationRequest): Promise<ValidationResult>;
signDepositPermission(
depositInfo: DepositInfo,
): Promise<CoinDepositPermission>;
deriveRefreshSession(
req: DeriveRefreshSessionRequest,
): Promise<DerivedRefreshSession>;
hashString(req: HashStringRequest): Promise<HashStringResult>;
signCoinLink(req: SignCoinLinkRequest): Promise<EddsaSigningResult>;
makeSyncSignature(req: MakeSyncSignatureRequest): Promise<EddsaSigningResult>;
}
/**
* Implementation of the Taler crypto interface where every function
* always throws. Only useful in practice as a way to iterate through
* all possible crypto functions.
*
* (This list can be easily auto-generated by your favorite IDE).
*/
export const nullCrypto: TalerCryptoInterface = {
createPlanchet: function (
req: PlanchetCreationRequest,
): Promise<WithdrawalPlanchet> {
throw new Error("Function not implemented.");
},
eddsaSign: function (req: EddsaSignRequest): Promise<EddsaSignResponse> {
throw new Error("Function not implemented.");
},
createTipPlanchet: function (
req: DeriveTipRequest,
): Promise<DerivedTipPlanchet> {
throw new Error("Function not implemented.");
},
signTrackTransaction: function (
req: SignTrackTransactionRequest,
): Promise<EddsaSigningResult> {
throw new Error("Function not implemented.");
},
createRecoupRequest: function (
req: CreateRecoupReqRequest,
): Promise<RecoupRequest> {
throw new Error("Function not implemented.");
},
createRecoupRefreshRequest: function (
req: CreateRecoupRefreshReqRequest,
): Promise<RecoupRefreshRequest> {
throw new Error("Function not implemented.");
},
isValidPaymentSignature: function (
req: PaymentSignatureValidationRequest,
): Promise<ValidationResult> {
throw new Error("Function not implemented.");
},
isValidWireFee: function (
req: WireFeeValidationRequest,
): Promise<ValidationResult> {
throw new Error("Function not implemented.");
},
isValidDenom: function (
req: DenominationValidationRequest,
): Promise<ValidationResult> {
throw new Error("Function not implemented.");
},
isValidWireAccount: function (
req: WireAccountValidationRequest,
): Promise<ValidationResult> {
throw new Error("Function not implemented.");
},
isValidContractTermsSignature: function (
req: ContractTermsValidationRequest,
): Promise<ValidationResult> {
throw new Error("Function not implemented.");
},
createEddsaKeypair: function (req: {}): Promise<EddsaKeypair> {
throw new Error("Function not implemented.");
},
eddsaGetPublic: function (req: EddsaGetPublicRequest): Promise<EddsaKeypair> {
throw new Error("Function not implemented.");
},
unblindDenominationSignature: function (
req: UnblindDenominationSignatureRequest,
): Promise<UnblindedSignature> {
throw new Error("Function not implemented.");
},
rsaUnblind: function (req: RsaUnblindRequest): Promise<RsaUnblindResponse> {
throw new Error("Function not implemented.");
},
rsaVerify: function (req: RsaVerificationRequest): Promise<ValidationResult> {
throw new Error("Function not implemented.");
},
signDepositPermission: function (
depositInfo: DepositInfo,
): Promise<CoinDepositPermission> {
throw new Error("Function not implemented.");
},
deriveRefreshSession: function (
req: DeriveRefreshSessionRequest,
): Promise<DerivedRefreshSession> {
throw new Error("Function not implemented.");
},
hashString: function (req: HashStringRequest): Promise<HashStringResult> {
throw new Error("Function not implemented.");
},
signCoinLink: function (
req: SignCoinLinkRequest,
): Promise<EddsaSigningResult> {
throw new Error("Function not implemented.");
},
makeSyncSignature: function (
req: MakeSyncSignatureRequest,
): Promise<EddsaSigningResult> {
throw new Error("Function not implemented.");
},
};
export type WithArg<X> = X extends (req: infer T) => infer R
? (tci: TalerCryptoInterfaceR, req: T) => R
: never;
export type TalerCryptoInterfaceR = {
[x in keyof TalerCryptoInterface]: WithArg<TalerCryptoInterface[x]>;
};
export interface SignCoinLinkRequest {
oldCoinPriv: string;
newDenomHash: string;
oldCoinPub: string;
transferPub: string;
coinEv: CoinEnvelope;
}
export interface RsaVerificationRequest {
hm: string;
sig: string;
pk: string;
}
export interface EddsaSigningResult {
sig: string;
}
export interface ValidationResult {
valid: boolean;
}
export interface HashStringRequest {
str: string;
}
export interface HashStringResult {
h: string;
}
export interface WireFeeValidationRequest {
type: string;
wf: WireFee;
masterPub: string;
}
export interface DenominationValidationRequest {
denom: DenominationRecord;
masterPub: string;
}
export interface PaymentSignatureValidationRequest {
sig: string;
contractHash: string;
merchantPub: string;
}
export interface ContractTermsValidationRequest {
contractTermsHash: string;
sig: string;
merchantPub: string;
}
export interface WireAccountValidationRequest {
versionCurrent: ExchangeProtocolVersion;
paytoUri: string;
sig: string;
masterPub: string;
}
export interface EddsaKeypair {
priv: string;
pub: string;
}
export interface EddsaGetPublicRequest {
priv: string;
}
export interface UnblindDenominationSignatureRequest {
planchet: PlanchetUnblindInfo;
evSig: BlindedDenominationSignature;
}
export interface RsaUnblindRequest {
blindedSig: string;
bk: string;
pk: string;
}
export interface RsaUnblindResponse {
sig: string;
}
export const nativeCryptoR: TalerCryptoInterfaceR = {
async eddsaSign(
tci: TalerCryptoInterfaceR,
req: EddsaSignRequest,
): Promise<EddsaSignResponse> {
return {
sig: encodeCrock(eddsaSign(decodeCrock(req.msg), decodeCrock(req.priv))),
};
},
async createPlanchet(
tci: TalerCryptoInterfaceR,
req: PlanchetCreationRequest,
): Promise<WithdrawalPlanchet> {
const denomPub = req.denomPub;
@ -195,7 +390,7 @@ export class CryptoImplementation {
.put(evHash)
.build();
const sigResult = await myEddsaSign(this.primitiveWorker, {
const sigResult = await tci.eddsaSign(tci, {
msg: encodeCrock(withdrawRequest),
priv: req.reservePriv,
});
@ -216,12 +411,12 @@ export class CryptoImplementation {
} else {
throw Error("unsupported cipher, unable to create planchet");
}
}
},
/**
* Create a planchet used for tipping, including the private keys.
*/
createTipPlanchet(req: DeriveTipRequest): DerivedTipPlanchet {
async createTipPlanchet(
tci: TalerCryptoInterfaceR,
req: DeriveTipRequest,
): Promise<DerivedTipPlanchet> {
if (req.denomPub.cipher !== DenomKeyType.Rsa) {
throw Error(`unsupported cipher (${req.denomPub.cipher})`);
}
@ -243,22 +438,28 @@ export class CryptoImplementation {
coinPub: encodeCrock(fc.coinPub),
};
return tipPlanchet;
}
},
signTrackTransaction(req: SignTrackTransactionRequest): string {
async signTrackTransaction(
tci: TalerCryptoInterfaceR,
req: SignTrackTransactionRequest,
): Promise<EddsaSigningResult> {
const p = buildSigPS(TalerSignaturePurpose.MERCHANT_TRACK_TRANSACTION)
.put(decodeCrock(req.contractTermsHash))
.put(decodeCrock(req.wireHash))
.put(decodeCrock(req.merchantPub))
.put(decodeCrock(req.coinPub))
.build();
return encodeCrock(eddsaSign(p, decodeCrock(req.merchantPriv)));
}
return { sig: encodeCrock(eddsaSign(p, decodeCrock(req.merchantPriv))) };
},
/**
* Create and sign a message to recoup a coin.
*/
createRecoupRequest(req: CreateRecoupReqRequest): RecoupRequest {
async createRecoupRequest(
tci: TalerCryptoInterfaceR,
req: CreateRecoupReqRequest,
): Promise<RecoupRequest> {
const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP)
.put(decodeCrock(req.denomPubHash))
.put(decodeCrock(req.blindingKey))
@ -281,14 +482,15 @@ export class CryptoImplementation {
} else {
throw new Error();
}
}
},
/**
* Create and sign a message to recoup a coin.
*/
createRecoupRefreshRequest(
async createRecoupRefreshRequest(
tci: TalerCryptoInterfaceR,
req: CreateRecoupRefreshReqRequest,
): RecoupRefreshRequest {
): Promise<RecoupRefreshRequest> {
const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP_REFRESH)
.put(decodeCrock(req.denomPubHash))
.put(decodeCrock(req.blindingKey))
@ -311,32 +513,32 @@ export class CryptoImplementation {
} else {
throw new Error();
}
}
},
/**
* Check if a payment signature is valid.
*/
isValidPaymentSignature(
sig: string,
contractHash: string,
merchantPub: string,
): boolean {
async isValidPaymentSignature(
tci: TalerCryptoInterfaceR,
req: PaymentSignatureValidationRequest,
): Promise<ValidationResult> {
const { contractHash, sig, merchantPub } = req;
const p = buildSigPS(TalerSignaturePurpose.MERCHANT_PAYMENT_OK)
.put(decodeCrock(contractHash))
.build();
const sigBytes = decodeCrock(sig);
const pubBytes = decodeCrock(merchantPub);
return eddsaVerify(p, sigBytes, pubBytes);
}
return { valid: eddsaVerify(p, sigBytes, pubBytes) };
},
/**
* Check if a wire fee is correctly signed.
*/
async isValidWireFee(
type: string,
wf: WireFee,
masterPub: string,
): Promise<boolean> {
tci: TalerCryptoInterfaceR,
req: WireFeeValidationRequest,
): Promise<ValidationResult> {
const { type, wf, masterPub } = req;
const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_FEES)
.put(hash(stringToBytes(type + "\0")))
.put(timestampRoundedToBuffer(wf.startStamp))
@ -347,25 +549,17 @@ export class CryptoImplementation {
.build();
const sig = decodeCrock(wf.sig);
const pub = decodeCrock(masterPub);
if (this.primitiveWorker) {
return (
await this.primitiveWorker.eddsaVerify({
msg: encodeCrock(p),
pub: masterPub,
sig: encodeCrock(sig),
})
).valid;
}
return eddsaVerify(p, sig, pub);
}
return { valid: eddsaVerify(p, sig, pub) };
},
/**
* Check if the signature of a denomination is valid.
*/
async isValidDenom(
denom: DenominationRecord,
masterPub: string,
): Promise<boolean> {
tci: TalerCryptoInterfaceR,
req: DenominationValidationRequest,
): Promise<ValidationResult> {
const { masterPub, denom } = req;
const p = buildSigPS(TalerSignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY)
.put(decodeCrock(masterPub))
.put(timestampRoundedToBuffer(denom.stampStart))
@ -382,56 +576,59 @@ export class CryptoImplementation {
const sig = decodeCrock(denom.masterSig);
const pub = decodeCrock(masterPub);
const res = eddsaVerify(p, sig, pub);
return res;
}
return { valid: res };
},
isValidWireAccount(
versionCurrent: ExchangeProtocolVersion,
paytoUri: string,
sig: string,
masterPub: string,
): boolean {
async isValidWireAccount(
tci: TalerCryptoInterfaceR,
req: WireAccountValidationRequest,
): Promise<ValidationResult> {
const { sig, masterPub, paytoUri } = req;
const paytoHash = hashTruncate32(stringToBytes(paytoUri + "\0"));
const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS)
.put(paytoHash)
.build();
return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub));
}
return { valid: eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub)) };
},
isValidContractTermsSignature(
contractTermsHash: string,
sig: string,
merchantPub: string,
): boolean {
const cthDec = decodeCrock(contractTermsHash);
async isValidContractTermsSignature(
tci: TalerCryptoInterfaceR,
req: ContractTermsValidationRequest,
): Promise<ValidationResult> {
const cthDec = decodeCrock(req.contractTermsHash);
const p = buildSigPS(TalerSignaturePurpose.MERCHANT_CONTRACT)
.put(cthDec)
.build();
return eddsaVerify(p, decodeCrock(sig), decodeCrock(merchantPub));
}
return {
valid: eddsaVerify(p, decodeCrock(req.sig), decodeCrock(req.merchantPub)),
};
},
/**
* Create a new EdDSA key pair.
*/
createEddsaKeypair(): { priv: string; pub: string } {
async createEddsaKeypair(tci: TalerCryptoInterfaceR): Promise<EddsaKeypair> {
const pair = createEddsaKeyPair();
return {
priv: encodeCrock(pair.eddsaPriv),
pub: encodeCrock(pair.eddsaPub),
};
}
},
eddsaGetPublic(key: string): { priv: string; pub: string } {
async eddsaGetPublic(
tci: TalerCryptoInterfaceR,
req: EddsaGetPublicRequest,
): Promise<EddsaKeypair> {
return {
priv: key,
pub: encodeCrock(eddsaGetPublic(decodeCrock(key))),
priv: req.priv,
pub: encodeCrock(eddsaGetPublic(decodeCrock(req.priv))),
};
}
},
unblindDenominationSignature(req: {
planchet: PlanchetUnblindInfo;
evSig: BlindedDenominationSignature;
}): UnblindedSignature {
async unblindDenominationSignature(
tci: TalerCryptoInterfaceR,
req: UnblindDenominationSignatureRequest,
): Promise<UnblindedSignature> {
if (req.evSig.cipher === DenomKeyType.Rsa) {
if (req.planchet.denomPub.cipher !== DenomKeyType.Rsa) {
throw new Error(
@ -450,32 +647,45 @@ export class CryptoImplementation {
} else {
throw Error(`unblinding for cipher ${req.evSig.cipher} not implemented`);
}
}
},
/**
* Unblind a blindly signed value.
*/
rsaUnblind(blindedSig: string, bk: string, pk: string): string {
async rsaUnblind(
tci: TalerCryptoInterfaceR,
req: RsaUnblindRequest,
): Promise<RsaUnblindResponse> {
const denomSig = rsaUnblind(
decodeCrock(blindedSig),
decodeCrock(pk),
decodeCrock(bk),
decodeCrock(req.blindedSig),
decodeCrock(req.pk),
decodeCrock(req.bk),
);
return encodeCrock(denomSig);
}
return { sig: encodeCrock(denomSig) };
},
/**
* Unblind a blindly signed value.
*/
rsaVerify(hm: string, sig: string, pk: string): boolean {
return rsaVerify(hash(decodeCrock(hm)), decodeCrock(sig), decodeCrock(pk));
}
async rsaVerify(
tci: TalerCryptoInterfaceR,
req: RsaVerificationRequest,
): Promise<ValidationResult> {
return {
valid: rsaVerify(
hash(decodeCrock(req.hm)),
decodeCrock(req.sig),
decodeCrock(req.pk),
),
};
},
/**
* Generate updated coins (to store in the database)
* and deposit permissions for each given coin.
*/
async signDepositPermission(
tci: TalerCryptoInterfaceR,
depositInfo: DepositInfo,
): Promise<CoinDepositPermission> {
// FIXME: put extensions here if used
@ -498,7 +708,7 @@ export class CryptoImplementation {
} else {
throw Error("unsupported exchange protocol version");
}
const coinSigRes = await myEddsaSign(this.primitiveWorker, {
const coinSigRes = await this.eddsaSign(tci, {
msg: encodeCrock(d),
priv: depositInfo.coinPriv,
});
@ -521,9 +731,10 @@ export class CryptoImplementation {
`unsupported denomination cipher (${depositInfo.denomKeyType})`,
);
}
}
},
async deriveRefreshSession(
tci: TalerCryptoInterfaceR,
req: DeriveRefreshSessionRequest,
): Promise<DerivedRefreshSession> {
const {
@ -596,24 +807,14 @@ export class CryptoImplementation {
let coinPub: Uint8Array;
let coinPriv: Uint8Array;
let blindingFactor: Uint8Array;
// disabled while not implemented in the C code
if (0 && this.primitiveWorker) {
const r = await this.primitiveWorker.setupRefreshPlanchet({
transfer_secret: encodeCrock(transferSecret),
coin_index: coinIndex,
});
coinPub = decodeCrock(r.coin_pub);
coinPriv = decodeCrock(r.coin_priv);
blindingFactor = decodeCrock(r.blinding_key);
} else {
let fresh: FreshCoin = setupRefreshPlanchet(
transferSecret,
coinIndex,
);
coinPriv = fresh.coinPriv;
coinPub = fresh.coinPub;
blindingFactor = fresh.bks;
}
// FIXME: make setupRefreshPlanchet a crypto api fn
let fresh: FreshCoin = setupRefreshPlanchet(
transferSecret,
coinIndex,
);
coinPriv = fresh.coinPriv;
coinPub = fresh.coinPub;
blindingFactor = fresh.bks;
const coinPubHash = hash(coinPub);
if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) {
throw Error("unsupported cipher, can't create refresh session");
@ -654,7 +855,7 @@ export class CryptoImplementation {
.put(amountToBuffer(meltFee))
.build();
const confirmSigResp = await myEddsaSign(this.primitiveWorker, {
const confirmSigResp = await tci.eddsaSign(tci, {
msg: encodeCrock(confirmData),
priv: meltCoinPriv,
});
@ -670,102 +871,42 @@ export class CryptoImplementation {
};
return refreshSession;
}
},
/**
* Hash a string including the zero terminator.
*/
hashString(str: string): string {
const b = stringToBytes(str + "\0");
return encodeCrock(hash(b));
}
/**
* Hash a crockford encoded value.
*/
hashEncoded(encodedBytes: string): string {
return encodeCrock(hash(decodeCrock(encodedBytes)));
}
async hashString(
tci: TalerCryptoInterfaceR,
req: HashStringRequest,
): Promise<HashStringResult> {
const b = stringToBytes(req.str + "\0");
return { h: encodeCrock(hash(b)) };
},
async signCoinLink(
oldCoinPriv: string,
newDenomHash: string,
oldCoinPub: string,
transferPub: string,
coinEv: CoinEnvelope,
): Promise<string> {
const coinEvHash = hashCoinEv(coinEv, newDenomHash);
tci: TalerCryptoInterfaceR,
req: SignCoinLinkRequest,
): Promise<EddsaSigningResult> {
const coinEvHash = hashCoinEv(req.coinEv, req.newDenomHash);
// FIXME: fill in
const hAgeCommitment = new Uint8Array(32);
const coinLink = buildSigPS(TalerSignaturePurpose.WALLET_COIN_LINK)
.put(decodeCrock(newDenomHash))
.put(decodeCrock(transferPub))
.put(decodeCrock(req.newDenomHash))
.put(decodeCrock(req.transferPub))
.put(hAgeCommitment)
.put(coinEvHash)
.build();
const sig = await myEddsaSign(this.primitiveWorker, {
return tci.eddsaSign(tci, {
msg: encodeCrock(coinLink),
priv: oldCoinPriv,
priv: req.oldCoinPriv,
});
return sig.sig;
}
},
benchmark(repetitions: number): BenchmarkResult {
let time_hash = BigInt(0);
for (let i = 0; i < repetitions; i++) {
const start = timer.performanceNow();
this.hashString("hello world");
time_hash += timer.performanceNow() - start;
}
let time_hash_big = BigInt(0);
for (let i = 0; i < repetitions; i++) {
const ba = randomBytes(4096);
const start = timer.performanceNow();
hash(ba);
time_hash_big += timer.performanceNow() - start;
}
let time_eddsa_create = BigInt(0);
for (let i = 0; i < repetitions; i++) {
const start = timer.performanceNow();
createEddsaKeyPair();
time_eddsa_create += timer.performanceNow() - start;
}
let time_eddsa_sign = BigInt(0);
const p = randomBytes(4096);
const pair = createEddsaKeyPair();
for (let i = 0; i < repetitions; i++) {
const start = timer.performanceNow();
eddsaSign(p, pair.eddsaPriv);
time_eddsa_sign += timer.performanceNow() - start;
}
const sig = eddsaSign(p, pair.eddsaPriv);
let time_eddsa_verify = BigInt(0);
for (let i = 0; i < repetitions; i++) {
const start = timer.performanceNow();
eddsaVerify(p, sig, pair.eddsaPub);
time_eddsa_verify += timer.performanceNow() - start;
}
return {
repetitions,
time: {
hash_small: Number(time_hash),
hash_big: Number(time_hash_big),
eddsa_create: Number(time_eddsa_create),
eddsa_sign: Number(time_eddsa_sign),
eddsa_verify: Number(time_eddsa_verify),
},
};
}
makeSyncSignature(req: MakeSyncSignatureRequest): string {
async makeSyncSignature(
tci: TalerCryptoInterfaceR,
req: MakeSyncSignatureRequest,
): Promise<EddsaSigningResult> {
const hNew = decodeCrock(req.newHash);
let hOld: Uint8Array;
if (req.oldHash) {
@ -778,6 +919,64 @@ export class CryptoImplementation {
.put(hNew)
.build();
const uploadSig = eddsaSign(sigBlob, decodeCrock(req.accountPriv));
return encodeCrock(uploadSig);
return { sig: encodeCrock(uploadSig) };
},
};
function amountToBuffer(amount: AmountJson): Uint8Array {
const buffer = new ArrayBuffer(8 + 4 + 12);
const dvbuf = new DataView(buffer);
const u8buf = new Uint8Array(buffer);
const curr = stringToBytes(amount.currency);
if (typeof dvbuf.setBigUint64 !== "undefined") {
dvbuf.setBigUint64(0, BigInt(amount.value));
} else {
const arr = bigint(amount.value).toArray(2 ** 8).value;
let offset = 8 - arr.length;
for (let i = 0; i < arr.length; i++) {
dvbuf.setUint8(offset++, arr[i]);
}
}
dvbuf.setUint32(8, amount.fraction);
u8buf.set(curr, 8 + 4);
return u8buf;
}
function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array {
const b = new ArrayBuffer(8);
const v = new DataView(b);
// The buffer we sign over represents the timestamp in microseconds.
if (typeof v.setBigUint64 !== "undefined") {
const s = BigInt(ts.t_s) * BigInt(1000 * 1000);
v.setBigUint64(0, s);
} else {
const s =
ts.t_s === "never" ? bigint.zero : bigint(ts.t_s).multiply(1000 * 1000);
const arr = s.toArray(2 ** 8).value;
let offset = 8 - arr.length;
for (let i = 0; i < arr.length; i++) {
v.setUint8(offset++, arr[i]);
}
}
return new Uint8Array(b);
}
export interface EddsaSignRequest {
msg: string;
priv: string;
}
export interface EddsaSignResponse {
sig: string;
}
export const nativeCrypto: TalerCryptoInterface = Object.fromEntries(
Object.keys(nativeCryptoR).map((name) => {
return [
name,
(req: any) =>
nativeCryptoR[name as keyof TalerCryptoInterfaceR](nativeCryptoR, req),
];
}),
) as any;

View File

@ -29,7 +29,6 @@
*/
import {
AmountJson,
AmountString,
CoinEnvelope,
DenominationPubKey,
ExchangeProtocolVersion,

View File

@ -22,39 +22,10 @@
/**
* Imports.
*/
import { DenominationRecord, WireFee } from "../../db.js";
import { CryptoWorker } from "./cryptoWorkerInterface.js";
import {
BlindedDenominationSignature,
CoinDepositPermission,
CoinEnvelope,
PlanchetUnblindInfo,
RecoupRefreshRequest,
RecoupRequest,
UnblindedSignature,
} from "@gnu-taler/taler-util";
import {
BenchmarkResult,
WithdrawalPlanchet,
PlanchetCreationRequest,
DepositInfo,
MakeSyncSignatureRequest,
} from "@gnu-taler/taler-util";
import * as timer from "../../util/timer.js";
import { Logger } from "@gnu-taler/taler-util";
import {
CreateRecoupRefreshReqRequest,
CreateRecoupReqRequest,
DerivedRefreshSession,
DerivedTipPlanchet,
DeriveRefreshSessionRequest,
DeriveTipRequest,
SignTrackTransactionRequest,
} from "../cryptoTypes.js";
import * as timer from "../../util/timer.js";
import { nullCrypto, TalerCryptoInterface } from "../cryptoImplementation.js";
import { CryptoWorker } from "./cryptoWorkerInterface.js";
const logger = new Logger("cryptoApi.ts");
@ -80,7 +51,7 @@ interface WorkerState {
interface WorkItem {
operation: string;
args: any[];
req: unknown;
resolve: any;
reject: any;
@ -122,10 +93,9 @@ export class CryptoApiStoppedError extends Error {
}
/**
* Crypto API that interfaces manages a background crypto thread
* for the execution of expensive operations.
* Dispatcher for cryptographic operations to underlying crypto workers.
*/
export class CryptoApi {
export class CryptoDispatcher {
private nextRpcId = 1;
private workers: WorkerState[];
private workQueues: WorkItem[][];
@ -191,7 +161,7 @@ export class CryptoApi {
}
const msg: any = {
args: work.args,
req: work.req,
id: work.rpcId,
operation: work.operation,
};
@ -277,7 +247,16 @@ export class CryptoApi {
currentWorkItem.resolve(msg.data.result);
}
cryptoApi: TalerCryptoInterface;
constructor(workerFactory: CryptoWorkerFactory) {
const fns: any = {};
for (const name of Object.keys(nullCrypto)) {
fns[name] = (x: any) => this.doRpc(name, 0, x);
}
this.cryptoApi = fns;
this.workerFactory = workerFactory;
this.workers = new Array<WorkerState>(workerFactory.getConcurrency());
@ -298,7 +277,7 @@ export class CryptoApi {
private doRpc<T>(
operation: string,
priority: number,
...args: any[]
req: unknown,
): Promise<T> {
if (this.stopped) {
throw new CryptoApiStoppedError();
@ -307,7 +286,7 @@ export class CryptoApi {
const rpcId = this.nextRpcId++;
const workItem: WorkItem = {
operation,
args,
req,
resolve,
reject,
rpcId,
@ -362,163 +341,4 @@ export class CryptoApi {
});
});
}
createPlanchet(req: PlanchetCreationRequest): Promise<WithdrawalPlanchet> {
return this.doRpc<WithdrawalPlanchet>("createPlanchet", 1, req);
}
unblindDenominationSignature(req: {
planchet: PlanchetUnblindInfo;
evSig: BlindedDenominationSignature;
}): Promise<UnblindedSignature> {
return this.doRpc<UnblindedSignature>(
"unblindDenominationSignature",
1,
req,
);
}
createTipPlanchet(req: DeriveTipRequest): Promise<DerivedTipPlanchet> {
return this.doRpc<DerivedTipPlanchet>("createTipPlanchet", 1, req);
}
signTrackTransaction(req: SignTrackTransactionRequest): Promise<string> {
return this.doRpc<string>("signTrackTransaction", 1, req);
}
hashString(str: string): Promise<string> {
return this.doRpc<string>("hashString", 1, str);
}
hashEncoded(encodedBytes: string): Promise<string> {
return this.doRpc<string>("hashEncoded", 1, encodedBytes);
}
isValidDenom(denom: DenominationRecord, masterPub: string): Promise<boolean> {
return this.doRpc<boolean>("isValidDenom", 2, denom, masterPub);
}
isValidWireFee(
type: string,
wf: WireFee,
masterPub: string,
): Promise<boolean> {
return this.doRpc<boolean>("isValidWireFee", 2, type, wf, masterPub);
}
isValidPaymentSignature(
sig: string,
contractHash: string,
merchantPub: string,
): Promise<boolean> {
return this.doRpc<boolean>(
"isValidPaymentSignature",
1,
sig,
contractHash,
merchantPub,
);
}
signDepositPermission(
depositInfo: DepositInfo,
): Promise<CoinDepositPermission> {
return this.doRpc<CoinDepositPermission>(
"signDepositPermission",
3,
depositInfo,
);
}
createEddsaKeypair(): Promise<{ priv: string; pub: string }> {
return this.doRpc<{ priv: string; pub: string }>("createEddsaKeypair", 1);
}
eddsaGetPublic(key: string): Promise<{ priv: string; pub: string }> {
return this.doRpc<{ priv: string; pub: string }>("eddsaGetPublic", 1, key);
}
rsaUnblind(sig: string, bk: string, pk: string): Promise<string> {
return this.doRpc<string>("rsaUnblind", 4, sig, bk, pk);
}
rsaVerify(hm: string, sig: string, pk: string): Promise<boolean> {
return this.doRpc<boolean>("rsaVerify", 4, hm, sig, pk);
}
isValidWireAccount(
versionCurrent: number,
paytoUri: string,
sig: string,
masterPub: string,
): Promise<boolean> {
return this.doRpc<boolean>(
"isValidWireAccount",
4,
versionCurrent,
paytoUri,
sig,
masterPub,
);
}
isValidContractTermsSignature(
contractTermsHash: string,
sig: string,
merchantPub: string,
): Promise<boolean> {
return this.doRpc<boolean>(
"isValidContractTermsSignature",
4,
contractTermsHash,
sig,
merchantPub,
);
}
createRecoupRequest(req: CreateRecoupReqRequest): Promise<RecoupRequest> {
return this.doRpc<RecoupRequest>("createRecoupRequest", 1, req);
}
createRecoupRefreshRequest(
req: CreateRecoupRefreshReqRequest,
): Promise<RecoupRefreshRequest> {
return this.doRpc<RecoupRefreshRequest>(
"createRecoupRefreshRequest",
1,
req,
);
}
deriveRefreshSession(
req: DeriveRefreshSessionRequest,
): Promise<DerivedRefreshSession> {
return this.doRpc<DerivedRefreshSession>("deriveRefreshSession", 4, req);
}
signCoinLink(
oldCoinPriv: string,
newDenomHash: string,
oldCoinPub: string,
transferPub: string,
coinEv: CoinEnvelope,
): Promise<string> {
return this.doRpc<string>(
"signCoinLink",
4,
oldCoinPriv,
newDenomHash,
oldCoinPub,
transferPub,
coinEv,
);
}
benchmark(repetitions: number): Promise<BenchmarkResult> {
return this.doRpc<BenchmarkResult>("benchmark", 1, repetitions);
}
makeSyncSignature(req: MakeSyncSignatureRequest): Promise<string> {
return this.doRpc<string>("makeSyncSignature", 3, req);
}
}

View File

@ -17,11 +17,11 @@
/**
* Imports
*/
import { CryptoWorkerFactory } from "./cryptoApi.js";
import { CryptoWorkerFactory } from "./cryptoDispatcher.js";
import { CryptoWorker } from "./cryptoWorkerInterface.js";
import os from "os";
import { CryptoImplementation } from "./cryptoImplementation.js";
import { Logger } from "@gnu-taler/taler-util";
import { nativeCryptoR } from "../cryptoImplementation.js";
const logger = new Logger("nodeThreadWorker.ts");
@ -69,9 +69,9 @@ const workerCode = `
* a message.
*/
export function handleWorkerMessage(msg: any): void {
const args = msg.args;
if (!Array.isArray(args)) {
console.error("args must be array");
const req = msg.req;
if (typeof req !== "object") {
console.error("request must be an object");
return;
}
const id = msg.id;
@ -86,7 +86,7 @@ export function handleWorkerMessage(msg: any): void {
}
const handleRequest = async (): Promise<void> => {
const impl = new CryptoImplementation();
const impl = nativeCryptoR;
if (!(operation in impl)) {
console.error(`crypto operation '${operation}' not found`);
@ -94,12 +94,11 @@ export function handleWorkerMessage(msg: any): void {
}
try {
const result = await (impl as any)[operation](...args);
const result = await (impl as any)[operation](impl, req);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const _r = "require";
const worker_threads: typeof import("worker_threads") = module[_r](
"worker_threads",
);
const worker_threads: typeof import("worker_threads") =
module[_r]("worker_threads");
// const worker_threads = require("worker_threads");
const p = worker_threads.parentPort;

View File

@ -0,0 +1,90 @@
/*
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 { Logger } from "@gnu-taler/taler-util";
import child_process from "child_process";
import type internal from "stream";
import { OpenedPromise, openPromise } from "../../util/promiseUtils.js";
const logger = new Logger("synchronousWorkerFactory.ts");
export class CryptoRpcClient {
proc: child_process.ChildProcessByStdio<
internal.Writable,
internal.Readable,
null
>;
requests: Array<{
p: OpenedPromise<any>;
req: any;
}> = [];
constructor() {
const stdoutChunks: Buffer[] = [];
this.proc = child_process.spawn("taler-crypto-worker", {
//stdio: ["pipe", "pipe", "inherit"],
stdio: ["pipe", "pipe", "inherit"],
detached: true,
});
this.proc.on("close", (): void => {
logger.error("child process exited");
});
(this.proc.stdout as any).unref();
(this.proc.stdin as any).unref();
this.proc.unref();
this.proc.stdout.on("data", (x) => {
// console.log("got chunk", x.toString("utf-8"));
if (x instanceof Buffer) {
const nlIndex = x.indexOf("\n");
if (nlIndex >= 0) {
const before = x.slice(0, nlIndex);
const after = x.slice(nlIndex + 1);
stdoutChunks.push(after);
const str = Buffer.concat([...stdoutChunks, before]).toString(
"utf-8",
);
const req = this.requests.shift();
if (!req) {
throw Error("request was undefined");
}
if (this.requests.length === 0) {
this.proc.unref();
}
//logger.info(`got response: ${str}`);
req.p.resolve(JSON.parse(str));
} else {
stdoutChunks.push(x);
}
} else {
throw Error(`unexpected data chunk type (${typeof x})`);
}
});
}
async queueRequest(req: any): Promise<any> {
const p = openPromise<any>();
if (this.requests.length === 0) {
this.proc.ref();
}
this.requests.push({ req, p });
this.proc.stdin.write(`${JSON.stringify(req)}\n`);
return p.promise;
}
}

View File

@ -16,11 +16,10 @@
import { Logger } from "@gnu-taler/taler-util";
import {
CryptoImplementation,
PrimitiveWorker
} from "./cryptoImplementation.js";
nativeCryptoR,
TalerCryptoInterfaceR,
} from "../cryptoImplementation.js";
import { CryptoRpcClient } from "./rpcClient.js";
const logger = new Logger("synchronousWorker.ts");
@ -38,9 +37,33 @@ export class SynchronousCryptoWorker {
*/
onerror: undefined | ((m: any) => void);
constructor(private primitiveWorker?: PrimitiveWorker) {
cryptoImplR: TalerCryptoInterfaceR;
rpcClient: CryptoRpcClient | undefined;
constructor() {
this.onerror = undefined;
this.onmessage = undefined;
this.cryptoImplR = { ...nativeCryptoR };
if (
process.env["TALER_WALLET_RPC_CRYPRO"] ||
// Old name
process.env["TALER_WALLET_PRIMITIVE_WORKER"]
) {
const rpc = (this.rpcClient = new CryptoRpcClient());
this.cryptoImplR.eddsaSign = async (_, req) => {
logger.trace("making RPC request");
return await rpc.queueRequest({
op: "eddsa_sign",
args: {
msg: req.msg,
priv: req.priv,
},
});
};
}
}
/**
@ -66,9 +89,9 @@ export class SynchronousCryptoWorker {
private async handleRequest(
operation: string,
id: number,
args: string[],
req: unknown,
): Promise<void> {
const impl = new CryptoImplementation(this.primitiveWorker);
const impl = this.cryptoImplR;
if (!(operation in impl)) {
console.error(`crypto operation '${operation}' not found`);
@ -77,7 +100,7 @@ export class SynchronousCryptoWorker {
let result: any;
try {
result = await (impl as any)[operation](...args);
result = await (impl as any)[operation](impl, req);
} catch (e) {
logger.error("error during operation", e);
return;
@ -94,9 +117,9 @@ export class SynchronousCryptoWorker {
* Send a message to the worker thread.
*/
postMessage(msg: any): void {
const args = msg.args;
if (!Array.isArray(args)) {
console.error("args must be array");
const req = msg.req;
if (typeof req !== "object") {
console.error("request must be an object");
return;
}
const id = msg.id;
@ -110,7 +133,7 @@ export class SynchronousCryptoWorker {
return;
}
this.handleRequest(operation, id, args).catch((e) => {
this.handleRequest(operation, id, req).catch((e) => {
console.error("Error while handling crypto request:", e);
});
}

View File

@ -14,121 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
PrimitiveWorker,
} from "./cryptoImplementation.js";
import { CryptoWorkerFactory } from "./cryptoApi.js";
/**
* Imports.
*/
import { CryptoWorkerFactory } from "./cryptoDispatcher.js";
import { CryptoWorker } from "./cryptoWorkerInterface.js";
import child_process from "child_process";
import type internal from "stream";
import { OpenedPromise, openPromise } from "../../index.js";
import { Logger } from "@gnu-taler/taler-util";
import { SynchronousCryptoWorker } from "./synchronousWorker.js";
const logger = new Logger("synchronousWorkerFactory.ts");
class MyPrimitiveWorker implements PrimitiveWorker {
proc: child_process.ChildProcessByStdio<
internal.Writable,
internal.Readable,
null
>;
requests: Array<{
p: OpenedPromise<any>;
req: any;
}> = [];
constructor() {
const stdoutChunks: Buffer[] = [];
this.proc = child_process.spawn("taler-crypto-worker", {
//stdio: ["pipe", "pipe", "inherit"],
stdio: ["pipe", "pipe", "inherit"],
detached: true,
});
this.proc.on("close", (): void => {
logger.error("child process exited");
});
(this.proc.stdout as any).unref();
(this.proc.stdin as any).unref();
this.proc.unref();
this.proc.stdout.on("data", (x) => {
// console.log("got chunk", x.toString("utf-8"));
if (x instanceof Buffer) {
const nlIndex = x.indexOf("\n");
if (nlIndex >= 0) {
const before = x.slice(0, nlIndex);
const after = x.slice(nlIndex + 1);
stdoutChunks.push(after);
const str = Buffer.concat([...stdoutChunks, before]).toString(
"utf-8",
);
const req = this.requests.shift();
if (!req) {
throw Error("request was undefined")
}
if (this.requests.length === 0) {
this.proc.unref();
}
//logger.info(`got response: ${str}`);
req.p.resolve(JSON.parse(str));
} else {
stdoutChunks.push(x);
}
} else {
throw Error(`unexpected data chunk type (${typeof x})`);
}
});
}
async setupRefreshPlanchet(req: {
transfer_secret: string;
coin_index: number;
}): Promise<{
coin_pub: string;
coin_priv: string;
blinding_key: string;
}> {
return this.queueRequest({
op: "setup_refresh_planchet",
args: req,
});
}
async queueRequest(req: any): Promise<any> {
const p = openPromise<any>();
if (this.requests.length === 0) {
this.proc.ref();
}
this.requests.push({ req, p });
this.proc.stdin.write(`${JSON.stringify(req)}\n`);
return p.promise;
}
async eddsaVerify(req: {
msg: string;
sig: string;
pub: string;
}): Promise<{ valid: boolean }> {
return this.queueRequest({
op: "eddsa_verify",
args: req,
});
}
async eddsaSign(req: {
msg: string;
priv: string;
}): Promise<{ sig: string }> {
return this.queueRequest({
op: "eddsa_sign",
args: req,
});
}
}
/**
* The synchronous crypto worker produced by this factory doesn't run in the
* background, but actually blocks the caller until the operation is done.
@ -139,12 +31,7 @@ export class SynchronousCryptoWorkerFactory implements CryptoWorkerFactory {
throw Error("cannot make worker, require(...) not defined");
}
let primitiveWorker;
if (process.env["TALER_WALLET_PRIMITIVE_WORKER"]) {
primitiveWorker = new MyPrimitiveWorker();
}
return new SynchronousCryptoWorker(primitiveWorker);
return new SynchronousCryptoWorker();
}
getConcurrency(): number {

View File

@ -47,10 +47,10 @@ import {
AbsoluteTime,
UnblindedSignature,
} from "@gnu-taler/taler-util";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import { DenominationRecord } from "./db.js";
import {
assembleRefreshRevealRequest,
CryptoApi,
ExchangeInfo,
getBankWithdrawalInfo,
HttpRequestLibrary,
@ -149,7 +149,7 @@ export async function topupReserveWithDemobank(
export async function withdrawCoin(args: {
http: HttpRequestLibrary;
cryptoApi: CryptoApi;
cryptoApi: TalerCryptoInterface;
reserveKeyPair: ReserveKeypair;
denom: DenominationRecord;
exchangeBaseUrl: string;
@ -212,7 +212,7 @@ export function findDenomOrThrow(
export async function depositCoin(args: {
http: HttpRequestLibrary;
cryptoApi: CryptoApi;
cryptoApi: TalerCryptoInterface;
exchangeBaseUrl: string;
coin: CoinInfo;
amount: AmountString;
@ -263,7 +263,7 @@ export async function depositCoin(args: {
export async function refreshCoin(req: {
http: HttpRequestLibrary;
cryptoApi: CryptoApi;
cryptoApi: TalerCryptoInterface;
oldCoin: CoinInfo;
newDenoms: DenominationRecord[];
}): Promise<void> {

View File

@ -25,7 +25,9 @@
import type { IDBFactory } from "@gnu-taler/idb-bridge";
// eslint-disable-next-line no-duplicate-imports
import {
BridgeIDBFactory, MemoryBackend, shimIndexedDB
BridgeIDBFactory,
MemoryBackend,
shimIndexedDB,
} from "@gnu-taler/idb-bridge";
import { AccessStats } from "@gnu-taler/idb-bridge/src/MemoryBackend";
import { Logger, WalletNotification } from "@gnu-taler/taler-util";

View File

@ -33,9 +33,11 @@ export * from "./db-utils.js";
// Crypto and crypto workers
// export * from "./crypto/workers/nodeThreadWorker.js";
export { CryptoImplementation } from "./crypto/workers/cryptoImplementation.js";
export type { CryptoWorker } from "./crypto/workers/cryptoWorkerInterface.js";
export { CryptoWorkerFactory, CryptoApi } from "./crypto/workers/cryptoApi.js";
export {
CryptoWorkerFactory,
CryptoDispatcher,
} from "./crypto/workers/cryptoDispatcher.js";
export { SynchronousCryptoWorker } from "./crypto/workers/synchronousWorker.js";
export * from "./pending-types.js";
@ -58,3 +60,8 @@ export * from "./operations/refresh.js";
export * from "./dbless.js";
export {
nativeCryptoR,
nativeCrypto,
nullCrypto,
} from "./crypto/cryptoImplementation.js";

View File

@ -36,7 +36,8 @@ import {
DenominationPubKey,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
import { CryptoApi } from "./crypto/workers/cryptoApi.js";
import { CryptoDispatcher } from "./crypto/workers/cryptoDispatcher.js";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import { ExchangeDetailsRecord, ExchangeRecord, WalletStoresV1 } from "./db.js";
import { PendingOperationsResponse } from "./pending-types.js";
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js";
@ -200,7 +201,7 @@ export interface InternalWalletState {
memoProcessRefresh: AsyncOpMemoMap<void>;
memoProcessRecoup: AsyncOpMemoMap<void>;
memoProcessDeposit: AsyncOpMemoMap<void>;
cryptoApi: CryptoApi;
cryptoApi: TalerCryptoInterface;
timerGroup: TimerGroup;
stopped: boolean;

View File

@ -69,7 +69,7 @@ import {
rsaBlind,
stringToBytes,
} from "@gnu-taler/taler-util";
import { CryptoApi } from "../../crypto/workers/cryptoApi.js";
import { CryptoDispatcher } from "../../crypto/workers/cryptoDispatcher.js";
import {
BackupProviderRecord,
BackupProviderState,
@ -99,6 +99,7 @@ import { exportBackup } from "./export.js";
import { BackupCryptoPrecomputedData, importBackup } from "./import.js";
import { getWalletBackupState, provideBackupState } from "./state.js";
import { guardOperationException } from "../common.js";
import { TalerCryptoInterface } from "../../crypto/cryptoImplementation.js";
const logger = new Logger("operations/backup.ts");
@ -154,7 +155,7 @@ export async function encryptBackup(
* FIXME: Move computations into crypto worker.
*/
async function computeBackupCryptoData(
cryptoApi: CryptoApi,
cryptoApi: TalerCryptoInterface,
backupContent: WalletBackupContentV1,
): Promise<BackupCryptoPrecomputedData> {
const cryptoData: BackupCryptoPrecomputedData = {
@ -193,18 +194,18 @@ async function computeBackupCryptoData(
}
}
for (const prop of backupContent.proposals) {
const contractTermsHash = await cryptoApi.hashString(
canonicalJson(prop.contract_terms_raw),
);
const { h: contractTermsHash } = await cryptoApi.hashString({
str: canonicalJson(prop.contract_terms_raw),
});
const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(prop.nonce_priv)));
cryptoData.proposalNoncePrivToPub[prop.nonce_priv] = noncePub;
cryptoData.proposalIdToContractTermsHash[prop.proposal_id] =
contractTermsHash;
}
for (const purch of backupContent.purchases) {
const contractTermsHash = await cryptoApi.hashString(
canonicalJson(purch.contract_terms_raw),
);
const { h: contractTermsHash } = await cryptoApi.hashString({
str: canonicalJson(purch.contract_terms_raw),
});
const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(purch.nonce_priv)));
cryptoData.proposalNoncePrivToPub[purch.nonce_priv] = noncePub;
cryptoData.proposalIdToContractTermsHash[purch.proposal_id] =
@ -286,13 +287,13 @@ async function runBackupCycleForProvider(
logger.trace(`trying to upload backup to ${provider.baseUrl}`);
logger.trace(`old hash ${oldHash}, new hash ${newHash}`);
const syncSig = await ws.cryptoApi.makeSyncSignature({
const syncSigResp = await ws.cryptoApi.makeSyncSignature({
newHash: encodeCrock(currentBackupHash),
oldHash: provider.lastBackupHash,
accountPriv: encodeCrock(accountKeyPair.eddsaPriv),
});
logger.trace(`sync signature is ${syncSig}`);
logger.trace(`sync signature is ${syncSigResp}`);
const accountBackupUrl = new URL(
`/backups/${encodeCrock(accountKeyPair.eddsaPub)}`,
@ -304,7 +305,7 @@ async function runBackupCycleForProvider(
body: encBackup,
headers: {
"content-type": "application/octet-stream",
"sync-signature": syncSig,
"sync-signature": syncSigResp.sig,
"if-none-match": newHash,
...(provider.lastBackupHash
? {

View File

@ -41,7 +41,7 @@ export async function provideBackupState(
}
// We need to generate the key outside of the transaction
// due to how IndexedDB works.
const k = await ws.cryptoApi.createEddsaKeypair();
const k = await ws.cryptoApi.createEddsaKeypair({});
const d = getRandomBytes(5);
// FIXME: device ID should be configured when wallet is initialized
// and be based on hostname

View File

@ -15,7 +15,7 @@
*/
import { TalerErrorDetail, TalerErrorCode } from "@gnu-taler/taler-util";
import { CryptoApiStoppedError } from "../crypto/workers/cryptoApi.js";
import { CryptoApiStoppedError } from "../crypto/workers/cryptoDispatcher.js";
import { TalerError, getErrorDetailFromException } from "../errors.js";
/**

View File

@ -254,14 +254,14 @@ export async function trackDepositGroup(
`deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${dp.coin_pub}`,
dp.exchange_url,
);
const sig = await ws.cryptoApi.signTrackTransaction({
const sigResp = await ws.cryptoApi.signTrackTransaction({
coinPub: dp.coin_pub,
contractTermsHash: depositGroup.contractTermsHash,
merchantPriv: depositGroup.merchantPriv,
merchantPub: depositGroup.merchantPub,
wireHash,
});
url.searchParams.set("merchant_sig", sig);
url.searchParams.set("merchant_sig", sigResp.sig);
const httpResp = await ws.http.get(url.href);
const body = await httpResp.json();
responses.push({
@ -391,8 +391,8 @@ export async function createDepositGroup(
const now = AbsoluteTime.now();
const nowRounded = AbsoluteTime.toTimestamp(now);
const noncePair = await ws.cryptoApi.createEddsaKeypair();
const merchantPair = await ws.cryptoApi.createEddsaKeypair();
const noncePair = await ws.cryptoApi.createEddsaKeypair({});
const merchantPair = await ws.cryptoApi.createEddsaKeypair({});
const wireSalt = encodeCrock(getRandomBytes(16));
const wireHash = hashWire(req.depositPaytoUri, wireSalt);
const contractTerms: ContractTerms = {
@ -421,9 +421,9 @@ export async function createDepositGroup(
refund_deadline: TalerProtocolTimestamp.zero(),
};
const contractTermsHash = await ws.cryptoApi.hashString(
canonicalJson(contractTerms),
);
const { h: contractTermsHash } = await ws.cryptoApi.hashString({
str: canonicalJson(contractTerms),
});
const contractData = extractContractData(
contractTerms,

View File

@ -46,7 +46,7 @@ import {
TalerProtocolDuration,
} from "@gnu-taler/taler-util";
import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util";
import { CryptoApi } from "../crypto/workers/cryptoApi.js";
import { CryptoDispatcher } from "../crypto/workers/cryptoDispatcher.js";
import {
DenominationRecord,
DenominationVerificationStatus,
@ -243,12 +243,13 @@ async function validateWireInfo(
if (ws.insecureTrustExchange) {
isValid = true;
} else {
isValid = await ws.cryptoApi.isValidWireAccount(
const { valid: v } = await ws.cryptoApi.isValidWireAccount({
masterPub: masterPublicKey,
paytoUri: a.payto_uri,
sig: a.master_sig,
versionCurrent,
a.payto_uri,
a.master_sig,
masterPublicKey,
);
});
isValid = v;
}
if (!isValid) {
throw Error("exchange acct signature invalid");
@ -272,11 +273,12 @@ async function validateWireInfo(
if (ws.insecureTrustExchange) {
isValid = true;
} else {
isValid = await ws.cryptoApi.isValidWireFee(
wireMethod,
fee,
masterPublicKey,
);
const { valid: v } = await ws.cryptoApi.isValidWireFee({
masterPub: masterPublicKey,
type: wireMethod,
wf: fee,
});
isValid = v;
}
if (!isValid) {
throw Error("exchange wire fee signature invalid");

View File

@ -55,7 +55,10 @@ import {
TransactionType,
URL,
} from "@gnu-taler/taler-util";
import { EXCHANGE_COINS_LOCK, InternalWalletState } from "../internal-wallet-state.js";
import {
EXCHANGE_COINS_LOCK,
InternalWalletState,
} from "../internal-wallet-state.js";
import {
AbortStatus,
AllowedAuditorInfo,
@ -100,6 +103,7 @@ import {
import { getExchangeDetails } from "./exchanges.js";
import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
import { guardOperationException } from "./common.js";
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
/**
* Logger.
@ -795,11 +799,11 @@ async function processDownloadProposalImpl(
);
}
const sigValid = await ws.cryptoApi.isValidContractTermsSignature(
const sigValid = await ws.cryptoApi.isValidContractTermsSignature({
contractTermsHash,
proposalResp.sig,
parsedContractTerms.merchant_pub,
);
merchantPub: parsedContractTerms.merchant_pub,
sig: proposalResp.sig,
});
if (!sigValid) {
const err = makeErrorDetail(
@ -921,9 +925,14 @@ async function startDownloadProposal(
return oldProposal.proposalId;
}
const { priv, pub } = await (noncePriv
? ws.cryptoApi.eddsaGetPublic(noncePriv)
: ws.cryptoApi.createEddsaKeypair());
let noncePair: EddsaKeypair;
if (noncePriv) {
noncePair = await ws.cryptoApi.eddsaGetPublic({ priv: noncePriv });
} else {
noncePair = await ws.cryptoApi.createEddsaKeypair({});
}
const { priv, pub } = noncePair;
const proposalId = encodeCrock(getRandomBytes(32));
const proposalRecord: ProposalRecord = {
@ -1673,11 +1682,11 @@ async function processPurchasePayImpl(
logger.trace("got success from pay URL", merchantResp);
const merchantPub = purchase.download.contractData.merchantPub;
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
merchantResp.sig,
purchase.download.contractData.contractTermsHash,
const { valid } = await ws.cryptoApi.isValidPaymentSignature({
contractHash: purchase.download.contractData.contractTermsHash,
merchantPub,
);
sig: merchantResp.sig,
});
if (!valid) {
logger.error("merchant payment signature invalid");

View File

@ -76,9 +76,9 @@ import {
RefreshNewDenomInfo,
} from "../crypto/cryptoTypes.js";
import { GetReadWriteAccess } from "../util/query.js";
import { CryptoApi } from "../index.browser.js";
import { guardOperationException } from "./common.js";
import { CryptoApiStoppedError } from "../crypto/workers/cryptoApi.js";
import { CryptoApiStoppedError } from "../crypto/workers/cryptoDispatcher.js";
import { TalerCryptoInterface } from "../crypto/cryptoImplementation.js";
const logger = new Logger("refresh.ts");
@ -461,7 +461,7 @@ async function refreshMelt(
}
export async function assembleRefreshRevealRequest(args: {
cryptoApi: CryptoApi;
cryptoApi: TalerCryptoInterface;
derived: DerivedRefreshSession;
norevealIndex: number;
oldCoinPub: CoinPublicKeyString;
@ -494,14 +494,14 @@ export async function assembleRefreshRevealRequest(args: {
const dsel = newDenoms[i];
for (let j = 0; j < dsel.count; j++) {
const newCoinIndex = linkSigs.length;
const linkSig = await cryptoApi.signCoinLink(
oldCoinPriv,
dsel.denomPubHash,
oldCoinPub,
derived.transferPubs[norevealIndex],
planchets[newCoinIndex].coinEv,
);
linkSigs.push(linkSig);
const linkSig = await cryptoApi.signCoinLink({
coinEv: planchets[newCoinIndex].coinEv,
newDenomHash: dsel.denomPubHash,
oldCoinPriv: oldCoinPriv,
oldCoinPub: oldCoinPub,
transferPub: derived.transferPubs[norevealIndex],
});
linkSigs.push(linkSig.sig);
newDenomsFlat.push(dsel.denomPubHash);
}
}

View File

@ -170,7 +170,7 @@ export async function createReserve(
ws: InternalWalletState,
req: CreateReserveRequest,
): Promise<CreateReserveResponse> {
const keypair = await ws.cryptoApi.createEddsaKeypair();
const keypair = await ws.cryptoApi.createEddsaKeypair({});
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
const canonExchange = canonicalizeBaseUrl(req.exchange);

View File

@ -336,17 +336,17 @@ async function processTipImpl(
throw Error("unsupported cipher");
}
const denomSigRsa = await ws.cryptoApi.rsaUnblind(
blindedSig.blinded_rsa_signature,
planchet.blindingKey,
denom.denomPub.rsa_public_key,
);
const denomSigRsa = await ws.cryptoApi.rsaUnblind({
bk: planchet.blindingKey,
blindedSig: blindedSig.blinded_rsa_signature,
pk: denom.denomPub.rsa_public_key,
});
const isValid = await ws.cryptoApi.rsaVerify(
planchet.coinPub,
denomSigRsa,
denom.denomPub.rsa_public_key,
);
const isValid = await ws.cryptoApi.rsaVerify({
hm: planchet.coinPub,
pk: denom.denomPub.rsa_public_key,
sig: denomSigRsa.sig,
});
if (!isValid) {
await ws.db
@ -377,7 +377,7 @@ async function processTipImpl(
},
currentAmount: denom.value,
denomPubHash: denom.denomPubHash,
denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa },
denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa.sig },
exchangeBaseUrl: tipRecord.exchangeBaseUrl,
status: CoinStatus.Fresh,
suspended: false,

View File

@ -603,17 +603,17 @@ async function processPlanchetVerifyAndStoreCoin(
throw Error("unsupported cipher");
}
const denomSigRsa = await ws.cryptoApi.rsaUnblind(
evSig.blinded_rsa_signature,
planchet.blindingKey,
planchetDenomPub.rsa_public_key,
);
const denomSigRsa = await ws.cryptoApi.rsaUnblind({
bk: planchet.blindingKey,
blindedSig: evSig.blinded_rsa_signature,
pk: planchetDenomPub.rsa_public_key,
});
const isValid = await ws.cryptoApi.rsaVerify(
planchet.coinPub,
denomSigRsa,
planchetDenomPub.rsa_public_key,
);
const isValid = await ws.cryptoApi.rsaVerify({
hm: planchet.coinPub,
pk: planchetDenomPub.rsa_public_key,
sig: denomSigRsa.sig,
});
if (!isValid) {
await ws.db
@ -640,7 +640,7 @@ async function processPlanchetVerifyAndStoreCoin(
if (planchetDenomPub.cipher === DenomKeyType.Rsa) {
denomSig = {
cipher: planchetDenomPub.cipher,
rsa_signature: denomSigRsa,
rsa_signature: denomSigRsa.sig,
};
} else {
throw Error("unsupported cipher");
@ -759,10 +759,11 @@ export async function updateWithdrawalDenoms(
if (ws.insecureTrustExchange) {
valid = true;
} else {
valid = await ws.cryptoApi.isValidDenom(
const res = await ws.cryptoApi.isValidDenom({
denom,
exchangeDetails.masterPublicKey,
);
masterPub: exchangeDetails.masterPublicKey,
});
valid = res.valid;
}
logger.trace(`Done validating ${denom.denomPubHash}`);
if (!valid) {

View File

@ -90,7 +90,10 @@ import {
RecoupOperations,
ReserveOperations,
} from "./internal-wallet-state.js";
import { CryptoApi, CryptoWorkerFactory } from "./crypto/workers/cryptoApi.js";
import {
CryptoDispatcher,
CryptoWorkerFactory,
} from "./crypto/workers/cryptoDispatcher.js";
import {
AuditorTrustRecord,
CoinSourceType,
@ -99,10 +102,7 @@ import {
ReserveRecordStatus,
WalletStoresV1,
} from "./db.js";
import {
getErrorDetailFromException,
TalerError,
} from "./errors.js";
import { getErrorDetailFromException, TalerError } from "./errors.js";
import { exportBackup } from "./operations/backup/export.js";
import {
addBackupProvider,
@ -193,6 +193,10 @@ import {
import { DbAccess, GetReadWriteAccess } from "./util/query.js";
import { TimerGroup } from "./util/timer.js";
import { WalletCoreApiClient } from "./wallet-api-types.js";
import {
TalerCryptoInterface,
TalerCryptoInterfaceR,
} from "./crypto/cryptoImplementation.js";
const builtinAuditors: AuditorTrustRecord[] = [
{
@ -1158,7 +1162,8 @@ class InternalWalletStateImpl implements InternalWalletState {
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
memoProcessDeposit: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
cryptoApi: CryptoApi;
cryptoApi: TalerCryptoInterface;
cryptoDispatcher: CryptoDispatcher;
merchantInfoCache: Record<string, MerchantInfo> = {};
@ -1213,7 +1218,8 @@ class InternalWalletStateImpl implements InternalWalletState {
public http: HttpRequestLibrary,
cryptoWorkerFactory: CryptoWorkerFactory,
) {
this.cryptoApi = new CryptoApi(cryptoWorkerFactory);
this.cryptoDispatcher = new CryptoDispatcher(cryptoWorkerFactory);
this.cryptoApi = this.cryptoDispatcher.cryptoApi;
}
async getDenomInfo(
@ -1258,7 +1264,7 @@ class InternalWalletStateImpl implements InternalWalletState {
stop(): void {
this.stopped = true;
this.timerGroup.stopCurrentAndFutureTimers();
this.cryptoApi.stop();
this.cryptoDispatcher.stop();
}
async runUntilDone(

View File

@ -23,18 +23,18 @@
*/
import { Logger } from "@gnu-taler/taler-util";
import { CryptoImplementation } from "@gnu-taler/taler-wallet-core";
import { nativeCrypto } from "@gnu-taler/taler-wallet-core";
const logger = new Logger("browserWorkerEntry.ts");
const worker: Worker = (self as any) as Worker;
const worker: Worker = self as any as Worker;
async function handleRequest(
operation: string,
id: number,
args: string[],
): Promise<void> {
const impl = new CryptoImplementation();
const impl = nativeCrypto;
if (!(operation in impl)) {
console.error(`crypto operation '${operation}' not found`);

View File

@ -97,10 +97,10 @@ export interface UpgradeResponse {
async function callBackend(operation: string, payload: any): Promise<any> {
let response: CoreApiResponse;
try {
response = await platform.setMessageToWalletBackground(operation, payload)
response = await platform.setMessageToWalletBackground(operation, payload);
} catch (e) {
console.log("Error calling backend");
throw new Error(`Error contacting backend: ${e}`)
throw new Error(`Error contacting backend: ${e}`);
}
console.log("got response", response);
if (response.type === "error") {
@ -413,12 +413,15 @@ export function importDB(dump: any): Promise<void> {
return callBackend("importDb", { dump });
}
export function onUpdateNotification(messageTypes: Array<NotificationType>, doCallback: () => void): () => void {
export function onUpdateNotification(
messageTypes: Array<NotificationType>,
doCallback: () => void,
): () => void {
const listener = (message: MessageFromBackend): void => {
const shouldNotify = messageTypes.includes(message.type);
if (shouldNotify) {
doCallback();
}
};
return platform.listenToWalletNotifications(listener)
return platform.listenToWalletNotifications(listener);
}