gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] 02/02: aml exchange API


From: gnunet
Subject: [taler-wallet-core] 02/02: aml exchange API
Date: Sun, 05 Nov 2023 22:20:34 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

commit 61563d1b4844caa3ac7d5d532b51309edd42101b
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Sun Nov 5 18:05:13 2023 -0300

    aml exchange API
---
 packages/taler-util/src/errors.ts                  |   4 +
 packages/taler-util/src/http-client/exchange.ts    | 125 ++++++++++++-
 packages/taler-util/src/http-client/merchant.ts    |   6 +
 .../taler-util/src/http-client/officer-account.ts  |  81 ++++++++
 packages/taler-util/src/http-client/types.ts       | 208 ++++++++++++++++++++-
 packages/taler-util/src/index.ts                   |   3 +
 packages/web-util/build.mjs                        |   1 +
 7 files changed, 417 insertions(+), 11 deletions(-)

diff --git a/packages/taler-util/src/errors.ts 
b/packages/taler-util/src/errors.ts
index cb61a5994..cbf4263fc 100644
--- a/packages/taler-util/src/errors.ts
+++ b/packages/taler-util/src/errors.ts
@@ -280,3 +280,7 @@ export function getErrorDetailFromException(e: any): 
TalerErrorDetail {
   );
   return err;
 }
+
+export function assertUnreachable(x: never): never {
+  throw new Error("Didn't expect to get here");
+}
diff --git a/packages/taler-util/src/http-client/exchange.ts 
b/packages/taler-util/src/http-client/exchange.ts
index 52f5dc5a6..2d3e40863 100644
--- a/packages/taler-util/src/http-client/exchange.ts
+++ b/packages/taler-util/src/http-client/exchange.ts
@@ -1,8 +1,12 @@
 import { HttpRequestLibrary } from "../http-common.js";
 import { HttpStatusCode } from "../http-status-codes.js";
 import { createPlatformHttpLib } from "../http.js";
-import { FailCasesByMethod, ResultByMethod, opSuccess, opUnknownFailure } from 
"../operation.js";
-import { codecForExchangeConfig } from "./types.js";
+import { LibtoolVersion } from "../libtool-version.js";
+import { hash } from "../nacl-fast.js";
+import { FailCasesByMethod, ResultByMethod, opEmptySuccess, opFixedSuccess, 
opKnownFailure, opSuccess, opUnknownFailure } from "../operation.js";
+import { TalerSignaturePurpose, amountToBuffer, bufferForUint32, buildSigPS, 
decodeCrock, eddsaSign, encodeCrock, stringToBytes, timestampRoundedToBuffer } 
from "../taler-crypto.js";
+import { OfficerAccount, PaginationParams, SigningKey, TalerExchangeApi, 
codecForAmlDecisionDetails, codecForAmlRecords, codecForExchangeConfig } from 
"./types.js";
+import { addPaginationParams } from "./utils.js";
 
 export type TalerExchangeResultByMethod<prop extends keyof 
TalerExchangeHttpClient> = ResultByMethod<TalerExchangeHttpClient, prop>
 export type TalerExchangeErrorsByMethod<prop extends keyof 
TalerExchangeHttpClient> = FailCasesByMethod<TalerExchangeHttpClient, prop>
@@ -11,6 +15,7 @@ export type TalerExchangeErrorsByMethod<prop extends keyof 
TalerExchangeHttpClie
  */
 export class TalerExchangeHttpClient {
   httpLib: HttpRequestLibrary;
+  public readonly PROTOCOL_VERSION = "17:0:0";
 
   constructor(
     readonly baseUrl: string,
@@ -19,6 +24,10 @@ export class TalerExchangeHttpClient {
     this.httpLib = httpClient ?? createPlatformHttpLib();
   }
 
+  isCompatible(version: string): boolean {
+    const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version)
+    return compare?.compatible ?? false
+  }
   /**
    * https://docs.taler.net/core/api-merchant.html#get--config
    * 
@@ -34,4 +43,116 @@ export class TalerExchangeHttpClient {
     }
   }
 
+  //
+  // AML operations
+  //
+
+  /**
+   * 
https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions-$STATE
+   * 
+   */
+  async getDecisionsByState(auth: OfficerAccount, state: 
TalerExchangeApi.AmlState, pagination?: PaginationParams) {
+    const url = new 
URL(`aml/${auth.id}/decisions/${TalerExchangeApi.AmlState[state]}`, 
this.baseUrl);
+    addPaginationParams(url, pagination)
+
+    const resp = await this.httpLib.fetch(url.href, {
+      method: "GET",
+      headers: {
+        "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey)
+      }
+    });
+
+    switch (resp.status) {
+      case HttpStatusCode.Ok: return opSuccess(resp, codecForAmlRecords())
+      case HttpStatusCode.NoContent: return opFixedSuccess({ records: [] })
+      //this should be unauthorized
+      case HttpStatusCode.Forbidden: return opKnownFailure("unauthorized", 
resp);
+      case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", 
resp);
+      case HttpStatusCode.NotFound: return opKnownFailure("officer-not-found", 
resp);
+      case HttpStatusCode.Conflict: return opKnownFailure("officer-disabled", 
resp);
+      default: return opUnknownFailure(resp, await resp.text())
+    }
+  }
+
+  /**
+   * 
https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decision-$H_PAYTO
+   * 
+   */
+  async getDecisionDetails(auth: OfficerAccount, account: string) {
+    const url = new URL(`aml/${auth.id}/decision/${account}`, this.baseUrl);
+
+    const resp = await this.httpLib.fetch(url.href, {
+      method: "GET",
+      headers: {
+        "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey)
+      }
+    });
+
+    switch (resp.status) {
+      case HttpStatusCode.Ok: return opSuccess(resp, 
codecForAmlDecisionDetails())
+      case HttpStatusCode.NoContent: return opFixedSuccess({ aml_history: [], 
kyc_attributes: [] })
+      //this should be unauthorized
+      case HttpStatusCode.Forbidden: return opKnownFailure("unauthorized", 
resp);
+      case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", 
resp);
+      case HttpStatusCode.NotFound: return opKnownFailure("officer-not-found", 
resp);
+      case HttpStatusCode.Conflict: return opKnownFailure("officer-disabled", 
resp);
+      default: return opUnknownFailure(resp, await resp.text())
+    }
+  }
+
+  /**
+   * 
https://docs.taler.net/core/api-exchange.html#post--aml-$OFFICER_PUB-decision
+   * 
+   */
+  async addDecisionDetails(auth: OfficerAccount, body: 
TalerExchangeApi.AmlDecision) {
+    const url = new URL(`aml/${auth.id}/decision`, this.baseUrl);
+
+    const resp = await this.httpLib.fetch(url.href, {
+      method: "POST",
+      body,
+      headers: {
+        "Taler-AML-Officer-Signature": buildDecisionSignature(auth.signingKey, 
body)
+      },
+    });
+
+    switch (resp.status) {
+      case HttpStatusCode.NoContent: return opEmptySuccess()
+      //FIXME: this should be unauthorized
+      case HttpStatusCode.Forbidden: return opKnownFailure("unauthorized", 
resp);
+      case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", 
resp);
+      //FIXME: this two need to be splitted by error code
+      case HttpStatusCode.NotFound: return 
opKnownFailure("officer-or-account-not-found", resp);
+      case HttpStatusCode.Conflict: return 
opKnownFailure("officer-disabled-or-recent-decision", resp);
+      default: return opUnknownFailure(resp, await resp.text())
+    }
+  }
+
+
+}
+
+function buildQuerySignature(key: SigningKey): string {
+  const sigBlob = buildSigPS(
+    TalerSignaturePurpose.TALER_SIGNATURE_AML_QUERY,
+  ).build();
+
+  return encodeCrock(eddsaSign(sigBlob, key));
+}
+
+function buildDecisionSignature(
+  key: SigningKey,
+  decision: TalerExchangeApi.AmlDecision,
+): string {
+  const zero = new Uint8Array(new ArrayBuffer(64))
+
+  const sigBlob = 
buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION)
+    //TODO: new need the null terminator, also in the exchange
+    .put(hash(stringToBytes(decision.justification)))//check null
+    .put(timestampRoundedToBuffer(decision.decision_time))
+    .put(amountToBuffer(decision.new_threshold))
+    .put(decodeCrock(decision.h_payto))
+    .put(zero) //kyc_requirement
+    .put(bufferForUint32(decision.new_state))
+    .build();
+
+  return encodeCrock(eddsaSign(sigBlob, key));
 }
\ No newline at end of file
diff --git a/packages/taler-util/src/http-client/merchant.ts 
b/packages/taler-util/src/http-client/merchant.ts
index 5aace2d78..a6dc4661f 100644
--- a/packages/taler-util/src/http-client/merchant.ts
+++ b/packages/taler-util/src/http-client/merchant.ts
@@ -1,6 +1,7 @@
 import { HttpRequestLibrary } from "../http-common.js";
 import { HttpStatusCode } from "../http-status-codes.js";
 import { createPlatformHttpLib } from "../http.js";
+import { LibtoolVersion } from "../libtool-version.js";
 import { FailCasesByMethod, ResultByMethod, opSuccess, opUnknownFailure } from 
"../operation.js";
 import { codecForMerchantConfig } from "./types.js";
 
@@ -11,6 +12,7 @@ export type TalerMerchantErrorsByMethod<prop extends keyof 
TalerMerchantHttpClie
  */
 export class TalerMerchantHttpClient {
   httpLib: HttpRequestLibrary;
+  public readonly PROTOCOL_VERSION = "5:0:1";
 
   constructor(
     readonly baseUrl: string,
@@ -19,6 +21,10 @@ export class TalerMerchantHttpClient {
     this.httpLib = httpClient ?? createPlatformHttpLib();
   }
 
+  isCompatible(version: string): boolean {
+    const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version)
+    return compare?.compatible ?? false
+  }
   /**
    * https://docs.taler.net/core/api-merchant.html#get--config
    * 
diff --git a/packages/taler-util/src/http-client/officer-account.ts 
b/packages/taler-util/src/http-client/officer-account.ts
new file mode 100644
index 000000000..4b2529e20
--- /dev/null
+++ b/packages/taler-util/src/http-client/officer-account.ts
@@ -0,0 +1,81 @@
+import {
+  LockedAccount,
+  OfficerAccount,
+  OfficerId,
+  SigningKey,
+  createEddsaKeyPair,
+  decodeCrock,
+  decryptWithDerivedKey,
+  eddsaGetPublic,
+  encodeCrock,
+  encryptWithDerivedKey,
+  getRandomBytesF,
+  stringToBytes
+} from "@gnu-taler/taler-util";
+
+/**
+ * Restore previous session and unlock account with password
+ *
+ * @param salt string from which crypto params will be derived
+ * @param key secured private key
+ * @param password password for the private key
+ * @returns
+ */
+export async function unlockOfficerAccount(
+  account: LockedAccount,
+  password: string,
+): Promise<OfficerAccount> {
+  const rawKey = decodeCrock(account);
+  const rawPassword = stringToBytes(password);
+
+  const signingKey = (await decryptWithDerivedKey(
+    rawKey,
+    rawPassword,
+    password,
+  ).catch((e: Error) => {
+    throw new UnwrapKeyError(e.message);
+  })) as SigningKey;
+
+  const publicKey = eddsaGetPublic(signingKey);
+
+  const accountId = encodeCrock(publicKey) as OfficerId;
+
+  return { id: accountId, signingKey };
+}
+
+/**
+ * Create new account (secured private key)
+ * secured with the given password
+ *
+ * @param sessionId
+ * @param password
+ * @returns
+ */
+export async function createNewOfficerAccount(
+  password: string,
+): Promise<OfficerAccount & { safe: LockedAccount }> {
+  const { eddsaPriv, eddsaPub } = createEddsaKeyPair();
+
+  const key = stringToBytes(password);
+
+  const protectedPrivKey = await encryptWithDerivedKey(
+    getRandomBytesF(24),
+    key,
+    eddsaPriv,
+    password,
+  );
+
+  const signingKey = eddsaPriv as SigningKey;
+  const accountId = encodeCrock(eddsaPub) as OfficerId;
+  const safe = encodeCrock(protectedPrivKey) as LockedAccount;
+
+  return { id: accountId, signingKey, safe };
+}
+
+export class UnwrapKeyError extends Error {
+  public cause: string;
+  constructor(cause: string) {
+    super(`Recovering private key failed on: ${cause}`);
+    this.cause = cause;
+  }
+}
diff --git a/packages/taler-util/src/http-client/types.ts 
b/packages/taler-util/src/http-client/types.ts
index fe69925f6..77004cf5b 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -1,10 +1,9 @@
 import { codecForAmountString } from "../amounts.js";
-import { Codec, buildCodecForObject, buildCodecForUnion, codecForBoolean, 
codecForConstString, codecForEither, codecForList, codecForMap, codecForNumber, 
codecForString, codecOptional } from "../codec.js";
-import { PaytoString, PaytoUri, codecForPaytoString } from "../payto.js";
+import { Codec, buildCodecForObject, buildCodecForUnion, codecForAny, 
codecForBoolean, codecForConstString, codecForEither, codecForList, 
codecForMap, codecForNumber, codecForString, codecOptional } from "../codec.js";
+import { PaytoString, codecForPaytoString } from "../payto.js";
 import { AmountString } from "../taler-types.js";
-import { TalerActionString, WithdrawUriResult, codecForTalerActionString } 
from "../taleruri.js";
+import { TalerActionString, codecForTalerActionString } from "../taleruri.js";
 import { codecForTimestamp } from "../time.js";
-import { TalerErrorDetail } from "../wallet-types.js";
 
 
 export type UserAndPassword = {
@@ -17,6 +16,22 @@ export type UserAndToken = {
   token: AccessToken,
 }
 
+declare const opaque_OfficerAccount: unique symbol;
+export type LockedAccount = string & { [opaque_OfficerAccount]: true };
+
+declare const opaque_OfficerId: unique symbol;
+export type OfficerId = string & { [opaque_OfficerId]: true };
+
+declare const opaque_OfficerSigningKey: unique symbol;
+export type SigningKey = Uint8Array & { [opaque_OfficerSigningKey]: true };
+
+
+export interface OfficerAccount {
+  id: OfficerId;
+  signingKey: SigningKey;
+}
+
+
 export type PaginationParams = {
   /**
    * row identifier as the starting point of the query
@@ -44,6 +59,10 @@ export type PaginationParams = {
 // 64-byte hash code.
 type HashCode = string;
 
+type PaytoHash = string;
+
+type AmlOfficerPublicKeyP = string;
+
 // 32-byte hash code.
 type ShortHashCode = string;
 
@@ -150,13 +169,18 @@ export interface LoginToken {
   token: AccessToken,
   expiration: Timestamp,
 }
-// token used to get loginToken
-// must forget after used
+
 declare const __ac_token: unique symbol;
 export type AccessToken = string & {
   [__ac_token]: true;
 };
 
+
+declare const __officer_signature: unique symbol;
+export type OfficerSignature = string & {
+  [__officer_signature]: true;
+};
+
 export namespace TalerAuthentication {
 
   export interface TokenRequest {
@@ -564,6 +588,66 @@ export const codecForAddIncomingResponse =
       .property("timestamp", codecForTimestamp)
       .build("TalerWireGatewayApi.AddIncomingResponse");
 
+export const codecForAmlRecords =
+  (): Codec<TalerExchangeApi.AmlRecords> =>
+    buildCodecForObject<TalerExchangeApi.AmlRecords>()
+      .property("records", codecForList(codecForAmlRecord()))
+      .build("TalerExchangeApi.PublicAccountsResponse");
+
+export const codecForAmlRecord =
+  (): Codec<TalerExchangeApi.AmlRecord> =>
+    buildCodecForObject<TalerExchangeApi.AmlRecord>()
+      .property("current_state", codecForNumber())
+      .property("h_payto", codecForString())
+      .property("rowid", codecForNumber())
+      .property("threshold", codecForAmountString())
+      .build("TalerExchangeApi.AmlRecord");
+
+export const codecForAmlDecisionDetails =
+  (): Codec<TalerExchangeApi.AmlDecisionDetails> =>
+    buildCodecForObject<TalerExchangeApi.AmlDecisionDetails>()
+      .property("aml_history", codecForList(codecForAmlDecisionDetail()))
+      .property("kyc_attributes", codecForList(codecForKycDetail()))
+      .build("TalerExchangeApi.AmlDecisionDetails");
+
+export const codecForAmlDecisionDetail =
+  (): Codec<TalerExchangeApi.AmlDecisionDetail> =>
+    buildCodecForObject<TalerExchangeApi.AmlDecisionDetail>()
+      .property("justification", codecForString())
+      .property("new_state", codecForNumber())
+      .property("decision_time", codecForTimestamp)
+      .property("new_threshold", codecForAmountString())
+      .property("decider_pub", codecForString())
+      .build("TalerExchangeApi.AmlDecisionDetail");
+
+interface KycDetail {
+  provider_section: string;
+  attributes?: Object;
+  collection_time: Timestamp;
+  expiration_time: Timestamp;
+}
+export const codecForKycDetail =
+  (): Codec<TalerExchangeApi.KycDetail> =>
+    buildCodecForObject<TalerExchangeApi.KycDetail>()
+      .property("provider_section", codecForString())
+      .property("attributes", codecOptional(codecForAny()))
+      .property("collection_time", codecForTimestamp)
+      .property("expiration_time", codecForTimestamp)
+      .build("TalerExchangeApi.KycDetail");
+
+export const codecForAmlDecision =
+  (): Codec<TalerExchangeApi.AmlDecision> =>
+    buildCodecForObject<TalerExchangeApi.AmlDecision>()
+      .property("justification", codecForString())
+      .property("new_threshold", codecForAmountString())
+      .property("h_payto", codecForString())
+      .property("new_state", codecForNumber())
+      .property("officer_sig", codecForString())
+      .property("decision_time", codecForTimestamp)
+      .property("kyc_requirements", 
codecOptional(codecForList(codecForString())))
+      .build("TalerExchangeApi.AmlDecision");
+
+
 // export const codecFor =
 //   (): Codec<TalerWireGatewayApi.PublicAccountsResponse> =>
 //     buildCodecForObject<TalerWireGatewayApi.PublicAccountsResponse>()
@@ -1341,6 +1425,112 @@ export namespace TalerCorebankApi {
 
 export namespace TalerExchangeApi {
 
+  export enum AmlState {
+    normal = 0,
+    pending = 1,
+    frozen = 2,
+  }
+
+  export interface AmlRecords {
+
+    // Array of AML records matching the query.
+    records: AmlRecord[];
+  }
+  export interface AmlRecord {
+
+    // Which payto-address is this record about.
+    // Identifies a GNU Taler wallet or an affected bank account.
+    h_payto: PaytoHash;
+
+    // What is the current AML state.
+    current_state: AmlState;
+
+    // Monthly transaction threshold before a review will be triggered
+    threshold: AmountString;
+
+    // RowID of the record.
+    rowid: Integer;
+
+  }
+
+  export interface AmlDecisionDetails {
+
+    // Array of AML decisions made for this account. Possibly
+    // contains only the most recent decision if "history" was
+    // not set to 'true'.
+    aml_history: AmlDecisionDetail[];
+
+    // Array of KYC attributes obtained for this account.
+    kyc_attributes: KycDetail[];
+  }
+  export interface AmlDecisionDetail {
+
+    // What was the justification given?
+    justification: string;
+
+    // What is the new AML state.
+    new_state: Integer;
+
+    // When was this decision made?
+    decision_time: Timestamp;
+
+    // What is the new AML decision threshold (in monthly transaction volume)?
+    new_threshold: AmountString;
+
+    // Who made the decision?
+    decider_pub: AmlOfficerPublicKeyP;
+
+  }
+  export interface KycDetail {
+
+    // Name of the configuration section that specifies the provider
+    // which was used to collect the KYC details
+    provider_section: string;
+
+    // The collected KYC data.  NULL if the attribute data could not
+    // be decrypted (internal error of the exchange, likely the
+    // attribute key was changed).
+    attributes?: Object;
+
+    // Time when the KYC data was collected
+    collection_time: Timestamp;
+
+    // Time when the validity of the KYC data will expire
+    expiration_time: Timestamp;
+
+  }
+
+
+  export interface AmlDecision {
+
+    // Human-readable justification for the decision.
+    justification: string;
+
+    // At what monthly transaction volume should the
+    // decision be automatically reviewed?
+    new_threshold: AmountString;
+
+    // Which payto-address is the decision about?
+    // Identifies a GNU Taler wallet or an affected bank account.
+    h_payto: PaytoHash;
+
+    // What is the new AML state (e.g. frozen, unfrozen, etc.)
+    // Numerical values are defined in AmlDecisionState.
+    new_state: Integer;
+
+    // Signature by the AML officer over a
+    // TALER_MasterAmlOfficerStatusPS.
+    // Must have purpose TALER_SIGNATURE_MASTER_AML_KEY.
+    officer_sig: EddsaSignature;
+
+    // When was the decision made?
+    decision_time: Timestamp;
+
+    // Optional argument to impose new KYC requirements
+    // that the customer has to satisfy to unblock transactions.
+    kyc_requirements?: string[];
+  }
+
 
   export interface ExchangeVersionResponse {
     // libtool-style representation of the Exchange protocol version, see
@@ -1362,19 +1552,19 @@ export namespace TalerExchangeApi {
 
   }
 
-  type AccountRestriction =
+  export type AccountRestriction =
     | RegexAccountRestriction
     | DenyAllAccountRestriction
   // Account restriction that disables this type of
   // account for the indicated operation categorically.
-  interface DenyAllAccountRestriction {
+  export interface DenyAllAccountRestriction {
 
     type: "deny";
   }
   // Accounts interacting with this type of account
   // restriction must have a payto://-URI matching
   // the given regex.
-  interface RegexAccountRestriction {
+  export interface RegexAccountRestriction {
 
     type: "regex";
 
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
index ea5a805a0..053a25ab7 100644
--- a/packages/taler-util/src/index.ts
+++ b/packages/taler-util/src/index.ts
@@ -43,6 +43,9 @@ export * from "./libeufin-api-types.js";
 export * from "./MerchantApiClient.js";
 export * from "./bank-api-client.js";
 export * from "./http-client/bank-core.js";
+export * from "./http-client/exchange.js";
+export * from "./http-client/merchant.js";
+export * from "./http-client/officer-account.js";
 export * from "./http-client/bank-integration.js";
 export * from "./http-client/bank-revenue.js";
 export * from "./http-client/bank-wire.js";
diff --git a/packages/web-util/build.mjs b/packages/web-util/build.mjs
index 0b015f22c..c15a2715b 100755
--- a/packages/web-util/build.mjs
+++ b/packages/web-util/build.mjs
@@ -59,6 +59,7 @@ const buildConfigBase = {
     ".key": "text",
     ".crt": "text",
     ".html": "text",
+    ".svg": "dataurl",
   },
   sourcemap: true,
   define: {

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]