diff options
author | Sebastian <sebasjm@gmail.com> | 2023-11-05 18:05:13 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-11-05 18:19:58 -0300 |
commit | 61563d1b4844caa3ac7d5d532b51309edd42101b (patch) | |
tree | f46794305a171dd8d6071b59a7050b8ba8048f93 /packages/taler-util/src/http-client | |
parent | b58d53dd93bd8e97aecc28fae788c5c7051fd73d (diff) | |
download | wallet-core-61563d1b4844caa3ac7d5d532b51309edd42101b.tar.xz |
aml exchange API
Diffstat (limited to 'packages/taler-util/src/http-client')
-rw-r--r-- | packages/taler-util/src/http-client/exchange.ts | 125 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/merchant.ts | 6 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/officer-account.ts | 81 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/types.ts | 208 |
4 files changed, 409 insertions, 11 deletions
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"; |