diff options
Diffstat (limited to 'packages/taler-util/src')
| -rw-r--r-- | packages/taler-util/src/talerCrypto.ts | 51 | ||||
| -rw-r--r-- | packages/taler-util/src/talerTypes.ts | 71 | ||||
| -rw-r--r-- | packages/taler-util/src/taleruri.ts | 66 | ||||
| -rw-r--r-- | packages/taler-util/src/walletTypes.ts | 81 | 
4 files changed, 240 insertions, 29 deletions
diff --git a/packages/taler-util/src/talerCrypto.ts b/packages/taler-util/src/talerCrypto.ts index 38bb5ad0a..d7734707a 100644 --- a/packages/taler-util/src/talerCrypto.ts +++ b/packages/taler-util/src/talerCrypto.ts @@ -1214,6 +1214,9 @@ type ContractPrivateKey = FlavorP<Uint8Array, "ContractPrivateKey", 32> &  type MergePrivateKey = FlavorP<Uint8Array, "MergePrivateKey", 32> &    MaterialEddsaPriv; +const mergeSalt = "p2p-merge-contract"; +const depositSalt = "p2p-deposit-contract"; +  export function encryptContractForMerge(    pursePub: PursePublicKey,    contractPriv: ContractPrivateKey, @@ -1230,12 +1233,24 @@ export function encryptContractForMerge(      contractTermsCompressed,    ]);    const key = keyExchangeEcdheEddsa(contractPriv, pursePub); -  return encryptWithDerivedKey( -    getRandomBytesF(24), -    key, -    data, -    "p2p-merge-contract", -  ); +  return encryptWithDerivedKey(getRandomBytesF(24), key, data, mergeSalt); +} + +export function encryptContractForDeposit( +  pursePub: PursePublicKey, +  contractPriv: ContractPrivateKey, +  contractTerms: any, +): Promise<OpaqueData> { +  const contractTermsCanon = canonicalJson(contractTerms) + "\0"; +  const contractTermsBytes = stringToBytes(contractTermsCanon); +  const contractTermsCompressed = fflate.zlibSync(contractTermsBytes); +  const data = typedArrayConcat([ +    bufferForUint32(ContractFormatTag.PaymentRequest), +    bufferForUint32(contractTermsBytes.length), +    contractTermsCompressed, +  ]); +  const key = keyExchangeEcdheEddsa(contractPriv, pursePub); +  return encryptWithDerivedKey(getRandomBytesF(24), key, data, depositSalt);  }  export interface DecryptForMergeResult { @@ -1243,13 +1258,17 @@ export interface DecryptForMergeResult {    mergePriv: Uint8Array;  } +export interface DecryptForDepositResult { +  contractTerms: any; +} +  export async function decryptContractForMerge(    enc: OpaqueData,    pursePub: PursePublicKey,    contractPriv: ContractPrivateKey,  ): Promise<DecryptForMergeResult> {    const key = keyExchangeEcdheEddsa(contractPriv, pursePub); -  const dec = await decryptWithDerivedKey(enc, key, "p2p-merge-contract"); +  const dec = await decryptWithDerivedKey(enc, key, mergeSalt);    const mergePriv = dec.slice(8, 8 + 32);    const contractTermsCompressed = dec.slice(8 + 32);    const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed); @@ -1263,6 +1282,20 @@ export async function decryptContractForMerge(    };  } -export function encryptContractForDeposit() { -  throw Error("not implemented"); +export async function decryptContractForDeposit( +  enc: OpaqueData, +  pursePub: PursePublicKey, +  contractPriv: ContractPrivateKey, +): Promise<DecryptForDepositResult> { +  const key = keyExchangeEcdheEddsa(contractPriv, pursePub); +  const dec = await decryptWithDerivedKey(enc, key, depositSalt); +  const contractTermsCompressed = dec.slice(8); +  const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed); +  // Slice of the '\0' at the end and decode to a string +  const contractTermsString = bytesToString( +    contractTermsBuf.slice(0, contractTermsBuf.length - 1), +  ); +  return { +    contractTerms: JSON.parse(contractTermsString), +  };  } diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts index d4de8c37b..ee2dee93c 100644 --- a/packages/taler-util/src/talerTypes.ts +++ b/packages/taler-util/src/talerTypes.ts @@ -1874,3 +1874,74 @@ export interface PeerContractTerms {    summary: string;    purse_expiration: TalerProtocolTimestamp;  } + +export interface EncryptedContract { +  // Encrypted contract. +  econtract: string; + +  // Signature over the (encrypted) contract. +  econtract_sig: string; + +  // Ephemeral public key for the DH operation to decrypt the encrypted contract. +  contract_pub: string; +} + +/** + * Payload for /reserves/{reserve_pub}/purse + * endpoint of the exchange. + */ +export interface ExchangeReservePurseRequest { +  /** +   * Minimum amount that must be credited to the reserve, that is +   * the total value of the purse minus the deposit fees. +   * If the deposit fees are lower, the contribution to the +   * reserve can be higher! +   */ +  purse_value: AmountString; + +  // Minimum age required for all coins deposited into the purse. +  min_age: number; + +  // Purse fee the reserve owner is willing to pay +  // for the purse creation. Optional, if not present +  // the purse is to be created from the purse quota +  // of the reserve. +  purse_fee: AmountString; + +  // Optional encrypted contract, in case the buyer is +  // proposing the contract and thus establishing the +  // purse with the payment. +  econtract?: EncryptedContract; + +  // EdDSA public key used to approve merges of this purse. +  merge_pub: EddsaPublicKeyString; + +  // EdDSA signature of the purse private key affirming the merge +  // over a TALER_PurseMergeSignaturePS. +  // Must be of purpose TALER_SIGNATURE_PURSE_MERGE. +  merge_sig: EddsaSignatureString; + +  // EdDSA signature of the account/reserve affirming the merge. +  // Must be of purpose TALER_SIGNATURE_WALLET_ACCOUNT_MERGE +  reserve_sig: EddsaSignatureString; + +  // Purse public key. +  purse_pub: EddsaPublicKeyString; + +  // EdDSA signature of the purse over +  // TALER_PurseRequestSignaturePS of +  // purpose TALER_SIGNATURE_PURSE_REQUEST +  // confirming that the +  // above details hold for this purse. +  purse_sig: EddsaSignatureString; + +  // SHA-512 hash of the contact of the purse. +  h_contract_terms: HashCodeString; + +  // Client-side timestamp of when the merge request was made. +  merge_timestamp: TalerProtocolTimestamp; + +  // Indicative time by which the purse should expire +  // if it has not been paid. +  purse_expiration: TalerProtocolTimestamp; +} diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts index e3bd120f0..e7d66d7d5 100644 --- a/packages/taler-util/src/taleruri.ts +++ b/packages/taler-util/src/taleruri.ts @@ -45,6 +45,11 @@ export interface PayPushUriResult {    contractPriv: string;  } +export interface PayPullUriResult { +  exchangeBaseUrl: string; +  contractPriv: string; +} +  /**   * Parse a taler[+http]://withdraw URI.   * Return undefined if not passed a valid URI. @@ -84,10 +89,14 @@ export enum TalerUriType {    TalerTip = "taler-tip",    TalerRefund = "taler-refund",    TalerNotifyReserve = "taler-notify-reserve", -  TalerPayPush = "pay-push", +  TalerPayPush = "taler-pay-push", +  TalerPayPull = "taler-pay-pull",    Unknown = "unknown",  } +const talerActionPayPull = "pay-pull"; +const talerActionPayPush = "pay-push"; +  /**   * Classify a taler:// URI.   */ @@ -117,12 +126,18 @@ export function classifyTalerUri(s: string): TalerUriType {    if (sl.startsWith("taler+http://withdraw/")) {      return TalerUriType.TalerWithdraw;    } -  if (sl.startsWith("taler://pay-push/")) { +  if (sl.startsWith(`taler://${talerActionPayPush}/`)) {      return TalerUriType.TalerPayPush;    } -  if (sl.startsWith("taler+http://pay-push/")) { +  if (sl.startsWith(`taler+http://${talerActionPayPush}/`)) {      return TalerUriType.TalerPayPush;    } +  if (sl.startsWith(`taler://${talerActionPayPull}/`)) { +    return TalerUriType.TalerPayPull; +  } +  if (sl.startsWith(`taler+http://${talerActionPayPull}/`)) { +    return TalerUriType.TalerPayPull; +  }    if (sl.startsWith("taler://notify-reserve/")) {      return TalerUriType.TalerNotifyReserve;    } @@ -189,7 +204,29 @@ export function parsePayUri(s: string): PayUriResult | undefined {  }  export function parsePayPushUri(s: string): PayPushUriResult | undefined { -  const pi = parseProtoInfo(s, "pay-push"); +  const pi = parseProtoInfo(s, talerActionPayPush); +  if (!pi) { +    return undefined; +  } +  const c = pi?.rest.split("?"); +  const parts = c[0].split("/"); +  if (parts.length < 2) { +    return undefined; +  } +  const host = parts[0].toLowerCase(); +  const contractPriv = parts[parts.length - 1]; +  const pathSegments = parts.slice(1, parts.length - 1); +  const p = [host, ...pathSegments].join("/"); +  const exchangeBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`); + +  return { +    exchangeBaseUrl, +    contractPriv, +  }; +} + +export function parsePayPullUri(s: string): PayPullUriResult | undefined { +  const pi = parseProtoInfo(s, talerActionPayPull);    if (!pi) {      return undefined;    } @@ -283,3 +320,24 @@ export function constructPayPushUri(args: {    }    return `${proto}://pay-push/${url.host}${url.pathname}${args.contractPriv}`;  } + +export function constructPayPullUri(args: { +  exchangeBaseUrl: string; +  contractPriv: string; +}): string { +  const url = new URL(args.exchangeBaseUrl); +  let proto: string; +  if (url.protocol === "https:") { +    proto = "taler"; +  } else if (url.protocol === "http:") { +    proto = "taler+http"; +  } else { +    throw Error(`Unsupported exchange URL protocol ${args.exchangeBaseUrl}`); +  } +  if (!url.pathname.endsWith("/")) { +    throw Error( +      `exchange base URL must end with a slash (got ${args.exchangeBaseUrl}instead)`, +    ); +  } +  return `${proto}://pay-pull/${url.host}${url.pathname}${args.contractPriv}`; +} diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts index 7b482c60e..3a415b221 100644 --- a/packages/taler-util/src/walletTypes.ts +++ b/packages/taler-util/src/walletTypes.ts @@ -627,7 +627,7 @@ export interface ExchangeAccount {    master_sig: string;  } -export type WireFeeMap = { [wireMethod: string]: WireFee[] } +export type WireFeeMap = { [wireMethod: string]: WireFee[] };  export interface WireInfo {    feesForType: WireFeeMap;    accounts: ExchangeAccount[]; @@ -639,7 +639,6 @@ const codecForExchangeAccount = (): Codec<ExchangeAccount> =>      .property("master_sig", codecForString())      .build("codecForExchangeAccount"); -  const codecForWireFee = (): Codec<WireFee> =>    buildCodecForObject<WireFee>()      .property("sig", codecForString()) @@ -658,19 +657,18 @@ const codecForWireInfo = (): Codec<WireInfo> =>  const codecForDenominationInfo = (): Codec<DenominationInfo> =>    buildCodecForObject<DenominationInfo>() -    .property("denomPubHash", (codecForString())) -    .property("value", (codecForAmountJson())) -    .property("feeWithdraw", (codecForAmountJson())) -    .property("feeDeposit", (codecForAmountJson())) -    .property("feeRefresh", (codecForAmountJson())) -    .property("feeRefund", (codecForAmountJson())) -    .property("stampStart", (codecForTimestamp)) -    .property("stampExpireWithdraw", (codecForTimestamp)) -    .property("stampExpireLegal", (codecForTimestamp)) -    .property("stampExpireDeposit", (codecForTimestamp)) +    .property("denomPubHash", codecForString()) +    .property("value", codecForAmountJson()) +    .property("feeWithdraw", codecForAmountJson()) +    .property("feeDeposit", codecForAmountJson()) +    .property("feeRefresh", codecForAmountJson()) +    .property("feeRefund", codecForAmountJson()) +    .property("stampStart", codecForTimestamp) +    .property("stampExpireWithdraw", codecForTimestamp) +    .property("stampExpireLegal", codecForTimestamp) +    .property("stampExpireDeposit", codecForTimestamp)      .build("codecForDenominationInfo"); -  export interface DenominationInfo {    value: AmountJson;    denomPubHash: string; @@ -713,7 +711,6 @@ export interface DenominationInfo {     * Data after which coins of this denomination can't be deposited anymore.     */    stampExpireDeposit: TalerProtocolTimestamp; -  }  export interface ExchangeListItem { @@ -726,7 +723,6 @@ export interface ExchangeListItem {    denominations: DenominationInfo[];  } -  const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>    buildCodecForObject<AuditorDenomSig>()      .property("denom_pub_h", codecForString()) @@ -740,7 +736,6 @@ const codecForExchangeAuditor = (): Codec<ExchangeAuditor> =>      .property("denomination_keys", codecForList(codecForAuditorDenomSig()))      .build("codecForExchangeAuditor"); -  const codecForExchangeTos = (): Codec<ExchangeTos> =>    buildCodecForObject<ExchangeTos>()      .property("acceptedVersion", codecOptional(codecForString())) @@ -1452,18 +1447,34 @@ export interface CheckPeerPushPaymentRequest {    talerUri: string;  } +export interface CheckPeerPullPaymentRequest { +  talerUri: string; +} +  export interface CheckPeerPushPaymentResponse {    contractTerms: any;    amount: AmountString;    peerPushPaymentIncomingId: string;  } +export interface CheckPeerPullPaymentResponse { +  contractTerms: any; +  amount: AmountString; +  peerPullPaymentIncomingId: string; +} +  export const codecForCheckPeerPushPaymentRequest =    (): Codec<CheckPeerPushPaymentRequest> =>      buildCodecForObject<CheckPeerPushPaymentRequest>()        .property("talerUri", codecForString())        .build("CheckPeerPushPaymentRequest"); +export const codecForCheckPeerPullPaymentRequest = +  (): Codec<CheckPeerPullPaymentRequest> => +    buildCodecForObject<CheckPeerPullPaymentRequest>() +      .property("talerUri", codecForString()) +      .build("CheckPeerPullPaymentRequest"); +  export interface AcceptPeerPushPaymentRequest {    /**     * Transparent identifier of the incoming peer push payment. @@ -1476,3 +1487,41 @@ export const codecForAcceptPeerPushPaymentRequest =      buildCodecForObject<AcceptPeerPushPaymentRequest>()        .property("peerPushPaymentIncomingId", codecForString())        .build("AcceptPeerPushPaymentRequest"); + +export interface AcceptPeerPullPaymentRequest { +  /** +   * Transparent identifier of the incoming peer pull payment. +   */ +  peerPullPaymentIncomingId: string; +} + +export const codecForAcceptPeerPullPaymentRequest = +  (): Codec<AcceptPeerPullPaymentRequest> => +    buildCodecForObject<AcceptPeerPullPaymentRequest>() +      .property("peerPullPaymentIncomingId", codecForString()) +      .build("AcceptPeerPllPaymentRequest"); + +export interface InitiatePeerPullPaymentRequest { +  /** +   * FIXME: Make this optional? +   */ +  exchangeBaseUrl: string; +  amount: AmountString; +  partialContractTerms: any; +} + +export const codecForInitiatePeerPullPaymentRequest = +  (): Codec<InitiatePeerPullPaymentRequest> => +    buildCodecForObject<InitiatePeerPullPaymentRequest>() +      .property("partialContractTerms", codecForAny()) +      .property("amount", codecForAmountString()) +      .property("exchangeBaseUrl", codecForAmountString()) +      .build("InitiatePeerPullPaymentRequest"); + +export interface InitiatePeerPullPaymentResponse { +  /** +   * Taler URI for the other party to make the payment +   * that was requested. +   */ +  talerUri: string; +}  | 
