aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-util/src/errors.ts4
-rw-r--r--packages/taler-util/src/http-client/exchange.ts125
-rw-r--r--packages/taler-util/src/http-client/merchant.ts6
-rw-r--r--packages/taler-util/src/http-client/officer-account.ts81
-rw-r--r--packages/taler-util/src/http-client/types.ts208
-rw-r--r--packages/taler-util/src/index.ts3
-rwxr-xr-xpackages/web-util/build.mjs1
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: {