reducer implementation WIP

This commit is contained in:
Florian Dold 2021-10-18 19:18:34 +02:00
parent 1b42529479
commit b1034801d1
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 1154 additions and 24 deletions

View File

@ -2,7 +2,9 @@
"name": "anastasis-core",
"version": "0.0.1",
"description": "",
"main": "index.js",
"main": "./lib/index.js",
"module": "./lib/index.js",
"types": "./lib/index.d.ts",
"scripts": {
"prepare": "tsc",
"compile": "tsc",
@ -20,7 +22,9 @@
},
"dependencies": {
"@gnu-taler/taler-util": "workspace:^0.8.3",
"hash-wasm": "^4.9.0"
"fetch-ponyfill": "^7.1.0",
"hash-wasm": "^4.9.0",
"node-fetch": "^3.0.0"
},
"ava": {
"files": [

View File

@ -1,15 +1,44 @@
import {
bytesToString,
canonicalJson,
decodeCrock,
encodeCrock,
getRandomBytes,
kdf,
secretbox,
stringToBytes,
} from "@gnu-taler/taler-util";
import { argon2id } from "hash-wasm";
export type Flavor<T, FlavorT> = T & { _flavor?: FlavorT };
export type FlavorP<T, FlavorT, S extends number> = T & {
_flavor?: FlavorT;
_size?: S;
};
export type UserIdentifier = Flavor<string, "UserIdentifier">;
export type ServerSalt = Flavor<string, "ServerSalt">;
export type PolicySalt = Flavor<string, "PolicySalt">;
export type PolicyKey = FlavorP<string, "PolicyKey", 64>;
export type KeyShare = Flavor<string, "KeyShare">;
export type EncryptedKeyShare = Flavor<string, "EncryptedKeyShare">;
export type EncryptedTruth = Flavor<string, "EncryptedTruth">;
export type EncryptedCoreSecret = Flavor<string, "EncryptedCoreSecret">;
export type EncryptedMasterKey = Flavor<string, "EncryptedMasterKey">;
/**
* Truth key, found in the recovery document.
*/
export type TruthKey = Flavor<string, "TruthKey">;
export type EncryptionNonce = Flavor<string, "EncryptionNonce">;
export type OpaqueData = Flavor<string, "OpaqueData">;
const nonceSize = 24;
const masterKeySize = 64;
export async function userIdentifierDerive(
idData: any,
serverSalt: string,
): Promise<string> {
serverSalt: ServerSalt,
): Promise<UserIdentifier> {
const canonIdData = canonicalJson(idData);
const hashInput = stringToBytes(canonIdData);
const result = await argon2id({
@ -24,15 +53,114 @@ export async function userIdentifierDerive(
return encodeCrock(result);
}
// interface Keypair {
// pub: string;
// priv: string;
// }
function taConcat(chunks: Uint8Array[]): Uint8Array {
let payloadLen = 0;
for (const c of chunks) {
payloadLen += c.byteLength;
}
const buf = new ArrayBuffer(payloadLen);
const u8buf = new Uint8Array(buf);
let p = 0;
for (const c of chunks) {
u8buf.set(c, p);
p += c.byteLength;
}
return u8buf;
}
// async function accountKeypairDerive(): Promise<Keypair> {}
export async function policyKeyDerive(
keyShares: KeyShare[],
policySalt: PolicySalt,
): Promise<PolicyKey> {
const chunks = keyShares.map((x) => decodeCrock(x));
const polKey = kdf(
64,
taConcat(chunks),
decodeCrock(policySalt),
new Uint8Array(0),
);
return encodeCrock(polKey);
}
// async function secureAnswerHash(
// answer: string,
// truthUuid: string,
// questionSalt: string,
// ): Promise<string> {}
async function deriveKey(
keySeed: OpaqueData,
nonce: EncryptionNonce,
salt: string,
): Promise<Uint8Array> {
return kdf(32, decodeCrock(keySeed), stringToBytes(salt), decodeCrock(nonce));
}
async function anastasisEncrypt(
nonce: EncryptionNonce,
keySeed: OpaqueData,
plaintext: OpaqueData,
salt: string,
): Promise<OpaqueData> {
const key = await deriveKey(keySeed, nonce, salt);
const nonceBuf = decodeCrock(nonce);
const cipherText = secretbox(decodeCrock(plaintext), decodeCrock(nonce), key);
return encodeCrock(taConcat([nonceBuf, cipherText]));
}
const asOpaque = (x: string): OpaqueData => x;
const asEncryptedKeyShare = (x: OpaqueData): EncryptedKeyShare => x as string;
const asEncryptedTruth = (x: OpaqueData): EncryptedTruth => x as string;
export async function encryptKeyshare(
keyShare: KeyShare,
userId: UserIdentifier,
answerSalt?: string,
): Promise<EncryptedKeyShare> {
const s = answerSalt ?? "eks";
const nonce = encodeCrock(getRandomBytes(24));
return asEncryptedKeyShare(
await anastasisEncrypt(nonce, asOpaque(userId), asOpaque(keyShare), s),
);
}
export async function encryptTruth(
nonce: EncryptionNonce,
truthEncKey: TruthKey,
truth: OpaqueData,
): Promise<EncryptedTruth> {
const salt = "ect";
return asEncryptedTruth(
await anastasisEncrypt(nonce, asOpaque(truthEncKey), truth, salt),
);
}
export interface CoreSecretEncResult {
encCoreSecret: EncryptedCoreSecret;
encMasterKeys: EncryptedMasterKey[];
}
export async function coreSecretEncrypt(
policyKeys: PolicyKey[],
coreSecret: OpaqueData,
): Promise<CoreSecretEncResult> {
const masterKey = getRandomBytes(masterKeySize);
const nonce = encodeCrock(getRandomBytes(nonceSize));
const coreSecretEncSalt = "cse";
const masterKeyEncSalt = "emk";
const encCoreSecret = (await anastasisEncrypt(
nonce,
encodeCrock(masterKey),
coreSecret,
coreSecretEncSalt,
)) as string;
const encMasterKeys: EncryptedMasterKey[] = [];
for (let i = 0; i < policyKeys.length; i++) {
const polNonce = encodeCrock(getRandomBytes(nonceSize));
const encMasterKey = await anastasisEncrypt(
polNonce,
asOpaque(policyKeys[i]),
encodeCrock(masterKey),
masterKeyEncSalt,
);
encMasterKeys.push(encMasterKey as string);
}
return {
encCoreSecret,
encMasterKeys,
};
}

View File

@ -1,14 +1,697 @@
import { md5, sha1, sha512, sha3 } from 'hash-wasm';
import {
AmountString,
codecForGetExchangeWithdrawalInfo,
decodeCrock,
encodeCrock,
getRandomBytes,
TalerErrorCode,
} from "@gnu-taler/taler-util";
import { anastasisData } from "./anastasis-data.js";
import {
EscrowConfigurationResponse,
TruthUploadRequest,
} from "./provider-types.js";
import {
ActionArgAddAuthentication,
ActionArgDeleteAuthentication,
ActionArgDeletePolicy,
ActionArgEnterSecret,
ActionArgEnterSecretName,
ActionArgEnterUserAttributes,
AuthenticationProviderStatus,
AuthenticationProviderStatusOk,
AuthMethod,
BackupStates,
ContinentInfo,
CountryInfo,
MethodSpec,
Policy,
PolicyProvider,
RecoveryStates,
ReducerState,
ReducerStateBackup,
ReducerStateBackupUserAttributesCollecting,
ReducerStateError,
ReducerStateRecovery,
} from "./reducer-types.js";
import fetchPonyfill from "fetch-ponyfill";
import {
coreSecretEncrypt,
encryptKeyshare,
encryptTruth,
PolicyKey,
policyKeyDerive,
UserIdentifier,
userIdentifierDerive,
} from "./crypto.js";
async function run() {
console.log('MD5:', await md5('demo'));
const { fetch, Request, Response, Headers } = fetchPonyfill({});
const int8Buffer = new Uint8Array([0, 1, 2, 3]);
console.log('SHA1:', await sha1(int8Buffer));
console.log('SHA512:', await sha512(int8Buffer));
export * from "./reducer-types.js";
const int32Buffer = new Uint32Array([1056, 641]);
console.log('SHA3-256:', await sha3(int32Buffer, 256));
interface RecoveryDocument {
// Human-readable name of the secret
secret_name?: string;
// Encrypted core secret.
encrypted_core_secret: string; // bytearray of undefined length
// List of escrow providers and selected authentication method.
escrow_methods: EscrowMethod[];
// List of possible decryption policies.
policies: DecryptionPolicy[];
}
run();
interface DecryptionPolicy {
// Salt included to encrypt master key share when
// using this decryption policy.
salt: string;
/**
* Master key, AES-encrypted with key derived from
* salt and keyshares revealed by the following list of
* escrow methods identified by UUID.
*/
master_key: string;
/**
* List of escrow methods identified by their UUID.
*/
uuid: string[];
}
interface EscrowMethod {
/**
* URL of the escrow provider (including possibly this Anastasis server).
*/
url: string;
/**
* Type of the escrow method (e.g. security question, SMS etc.).
*/
escrow_type: string;
// UUID of the escrow method (see /truth/ API below).
// 16 bytes base32-crock encoded.
uuid: string;
// Key used to encrypt the Truth this EscrowMethod is related to.
// Client has to provide this key to the server when using /truth/.
truth_key: string;
// Salt used to encrypt the truth on the Anastasis server.
salt: string;
// Salt from the provider to derive the user ID
// at this provider.
provider_salt: string;
// The instructions to give to the user (i.e. the security question
// if this is challenge-response).
// (Q: as string in base32 encoding?)
// (Q: what is the mime-type of this value?)
//
// The plaintext challenge is not revealed to the
// Anastasis server.
instructions: string;
}
function getContinents(): ContinentInfo[] {
const continentSet = new Set<string>();
const continents: ContinentInfo[] = [];
for (const country of anastasisData.countriesList.countries) {
if (continentSet.has(country.continent)) {
continue;
}
continentSet.add(country.continent);
continents.push({
...{ name_i18n: country.continent_i18n },
name: country.continent,
});
}
return continents;
}
function getCountries(continent: string): CountryInfo[] {
return anastasisData.countriesList.countries.filter(
(x) => x.continent === continent,
);
}
export async function getBackupStartState(): Promise<ReducerStateBackup> {
return {
backup_state: BackupStates.ContinentSelecting,
continents: getContinents(),
};
}
export async function getRecoveryStartState(): Promise<ReducerStateRecovery> {
return {
recovery_state: RecoveryStates.ContinentSelecting,
continents: getContinents(),
};
}
async function backupSelectCountry(
state: ReducerStateBackup,
countryCode: string,
currencies: string[],
): Promise<ReducerStateError | ReducerStateBackupUserAttributesCollecting> {
const country = anastasisData.countriesList.countries.find(
(x) => x.code === countryCode,
);
if (!country) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: "invalid country selected",
};
}
const providers: { [x: string]: {} } = {};
for (const prov of anastasisData.providersList.anastasis_provider) {
if (currencies.includes(prov.currency)) {
providers[prov.url] = {};
}
}
const ra = (anastasisData.countryDetails as any)[countryCode]
.required_attributes;
return {
...state,
backup_state: BackupStates.UserAttributesCollecting,
selected_country: countryCode,
currencies,
required_attributes: ra,
authentication_providers: providers,
};
}
async function getProviderInfo(
providerBaseUrl: string,
): Promise<AuthenticationProviderStatus> {
// FIXME: Use a reasonable timeout here.
let resp: Response;
try {
resp = await fetch(new URL("config", providerBaseUrl).href);
} catch (e) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
hint: "request to provider failed",
};
}
if (resp.status !== 200) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
hint: "unexpected status",
http_status: resp.status,
};
}
try {
const jsonResp: EscrowConfigurationResponse = await resp.json();
return {
http_status: 200,
annual_fee: jsonResp.annual_fee,
business_name: jsonResp.business_name,
currency: jsonResp.currency,
liability_limit: jsonResp.liability_limit,
methods: jsonResp.methods.map((x) => ({
type: x.type,
usage_fee: x.cost,
})),
salt: jsonResp.server_salt,
storage_limit_in_megabytes: jsonResp.storage_limit_in_megabytes,
truth_upload_fee: jsonResp.truth_upload_fee,
} as AuthenticationProviderStatusOk;
} catch (e) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
hint: "provider did not return JSON",
};
}
}
async function backupEnterUserAttributes(
state: ReducerStateBackup,
attributes: Record<string, string>,
): Promise<ReducerStateBackup> {
const providerUrls = Object.keys(state.authentication_providers ?? {});
const newProviders = state.authentication_providers ?? {};
for (const url of providerUrls) {
newProviders[url] = await getProviderInfo(url);
}
const newState = {
...state,
backup_state: BackupStates.AuthenticationsEditing,
authentication_providers: newProviders,
identity_attributes: attributes,
};
return newState;
}
interface PolicySelectionResult {
policies: Policy[];
policy_providers: PolicyProvider[];
}
type MethodSelection = number[];
function enumerateSelections(n: number, m: number): MethodSelection[] {
const selections: MethodSelection[] = [];
const a = new Array(n);
const sel = (i: number) => {
if (i === n) {
selections.push([...a]);
return;
}
const start = i == 0 ? 0 : a[i - 1] + 1;
for (let j = start; j < m; j++) {
a[i] = j;
sel(i + 1);
}
};
sel(0);
return selections;
}
/**
* Provider information used during provider/method mapping.
*/
interface ProviderInfo {
url: string;
methodCost: Record<string, AmountString>;
}
/**
* Assign providers to a method selection.
*/
function assignProviders(
methods: AuthMethod[],
providers: ProviderInfo[],
methodSelection: number[],
): Policy | undefined {
const selectedProviders: string[] = [];
for (const mi of methodSelection) {
const m = methods[mi];
let found = false;
for (const prov of providers) {
if (prov.methodCost[m.type]) {
selectedProviders.push(prov.url);
found = true;
break;
}
}
if (!found) {
/* No provider found for this method */
return undefined;
}
}
return {
methods: methodSelection.map((x, i) => {
return {
authentication_method: x,
provider: selectedProviders[i],
};
}),
};
}
function suggestPolicies(
methods: AuthMethod[],
providers: ProviderInfo[],
): PolicySelectionResult {
const numMethods = methods.length;
if (numMethods === 0) {
throw Error("no methods");
}
let numSel: number;
if (numMethods <= 2) {
numSel = numMethods;
} else if (numMethods <= 4) {
numSel = numMethods - 1;
} else if (numMethods <= 6) {
numSel = numMethods - 2;
} else if (numMethods == 7) {
numSel = numMethods - 3;
} else {
numSel = 4;
}
const policies: Policy[] = [];
const selections = enumerateSelections(numSel, numMethods);
console.log("selections", selections);
for (const sel of selections) {
const p = assignProviders(methods, providers, sel);
if (p) {
policies.push(p);
}
}
return {
policies,
policy_providers: providers.map((x) => ({
provider_url: x.url,
})),
};
}
/**
* Truth data as stored in the reducer.
*/
interface TruthMetaData {
uuid: string;
key_share: string;
policy_index: number;
pol_method_index: number;
/**
* Nonce used for encrypting the truth.
*/
nonce: string;
/**
* Key that the truth (i.e. secret question answer, email address, mobile number, ...)
* is encrypted with when stored at the provider.
*/
truth_key: string;
/**
* Truth-specific salt.
*/
salt: string;
}
async function uploadSecret(
state: ReducerStateBackup,
): Promise<ReducerStateBackup | ReducerStateError> {
const policies = state.policies!;
const secretName = state.secret_name!;
const coreSecret = state.core_secret?.value!;
// Truth key is `${methodIndex}/${providerUrl}`
const truthMetadataMap: Record<string, TruthMetaData> = {};
const policyKeys: PolicyKey[] = [];
for (let policyIndex = 0; policyIndex < policies.length; policyIndex++) {
const pol = policies[policyIndex];
const policySalt = encodeCrock(getRandomBytes(64));
const keyShares: string[] = [];
for (let methIndex = 0; methIndex < pol.methods.length; methIndex++) {
const meth = pol.methods[methIndex];
const truthKey = `${meth.authentication_method}:${meth.provider}`;
if (truthMetadataMap[truthKey]) {
continue;
}
const keyShare = encodeCrock(getRandomBytes(32));
keyShares.push(keyShare);
const tm: TruthMetaData = {
key_share: keyShare,
nonce: encodeCrock(getRandomBytes(24)),
salt: encodeCrock(getRandomBytes(16)),
truth_key: encodeCrock(getRandomBytes(32)),
uuid: encodeCrock(getRandomBytes(32)),
pol_method_index: methIndex,
policy_index: policyIndex,
};
truthMetadataMap[truthKey] = tm;
}
const policyKey = await policyKeyDerive(keyShares, policySalt);
policyKeys.push(policyKey);
}
const csr = await coreSecretEncrypt(policyKeys, coreSecret);
const uidMap: Record<string, UserIdentifier> = {};
for (const prov of state.policy_providers!) {
const provider = state.authentication_providers![
prov.provider_url
] as AuthenticationProviderStatusOk;
uidMap[prov.provider_url] = await userIdentifierDerive(
state.identity_attributes!,
provider.salt,
);
}
const escrowMethods: EscrowMethod[] = [];
for (const truthKey of Object.keys(truthMetadataMap)) {
const tm = truthMetadataMap[truthKey];
const pol = state.policies![tm.policy_index];
const meth = pol.methods[tm.pol_method_index];
const authMethod =
state.authentication_methods![meth.authentication_method];
const provider = state.authentication_providers![
meth.provider
] as AuthenticationProviderStatusOk;
const encryptedTruth = await encryptTruth(
tm.nonce,
tm.truth_key,
authMethod.challenge,
);
const uid = uidMap[meth.provider];
const encryptedKeyShare = await encryptKeyshare(tm.key_share, uid, tm.salt);
console.log(
"encrypted key share len",
decodeCrock(encryptedKeyShare).length,
);
const tur: TruthUploadRequest = {
encrypted_truth: encryptedTruth,
key_share_data: encryptedKeyShare,
storage_duration_years: 5 /* FIXME */,
type: authMethod.type,
truth_mime: authMethod.mime_type,
};
const resp = await fetch(new URL(`truth/${tm.uuid}`, meth.provider).href, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(tur),
});
escrowMethods.push({
escrow_type: authMethod.type,
instructions: authMethod.instructions,
provider_salt: provider.salt,
salt: tm.salt,
truth_key: tm.truth_key,
url: meth.provider,
uuid: tm.uuid,
});
}
// FIXME: We need to store the truth metadata in
// the state, since it's possible that we'll run into
// a provider that requests a payment.
const rd: RecoveryDocument = {
secret_name: secretName,
encrypted_core_secret: csr.encCoreSecret,
escrow_methods: escrowMethods,
policies: policies.map((x, i) => {
return {
master_key: csr.encMasterKeys[i],
uuid: [],
salt:
};
}),
};
for (const prov of state.policy_providers!) {
// FIXME: Upload recovery document.
}
return {
code: 123,
hint: "not implemented",
};
}
export async function reduceAction(
state: ReducerState,
action: string,
args: any,
): Promise<ReducerState> {
console.log(`ts reducer: handling action ${action}`);
if (state.backup_state === BackupStates.ContinentSelecting) {
if (action === "select_continent") {
const continent: string = args.continent;
if (typeof continent !== "string") {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: "continent required",
};
}
return {
...state,
backup_state: BackupStates.CountrySelecting,
countries: getCountries(continent),
selected_continent: continent,
};
} else {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: `Unsupported action '${action}'`,
};
}
}
if (state.backup_state === BackupStates.CountrySelecting) {
if (action === "back") {
return {
...state,
backup_state: BackupStates.ContinentSelecting,
countries: undefined,
};
} else if (action === "select_country") {
const countryCode = args.country_code;
if (typeof countryCode !== "string") {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: "country_code required",
};
}
const currencies = args.currencies;
return backupSelectCountry(state, countryCode, currencies);
} else {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: `Unsupported action '${action}'`,
};
}
}
if (state.backup_state === BackupStates.UserAttributesCollecting) {
if (action === "back") {
return {
...state,
backup_state: BackupStates.CountrySelecting,
};
} else if (action === "enter_user_attributes") {
const ta = args as ActionArgEnterUserAttributes;
return backupEnterUserAttributes(state, ta.identity_attributes);
} else {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: `Unsupported action '${action}'`,
};
}
}
if (state.backup_state === BackupStates.AuthenticationsEditing) {
if (action === "back") {
return {
...state,
backup_state: BackupStates.UserAttributesCollecting,
};
} else if (action === "add_authentication") {
const ta = args as ActionArgAddAuthentication;
return {
...state,
authentication_methods: [
...(state.authentication_methods ?? []),
ta.authentication_method,
],
};
} else if (action === "delete_authentication") {
const ta = args as ActionArgDeleteAuthentication;
const m = state.authentication_methods ?? [];
m.splice(ta.authentication_method, 1);
return {
...state,
authentication_methods: m,
};
} else if (action === "next") {
const methods = state.authentication_methods ?? [];
const providers: ProviderInfo[] = [];
for (const provUrl of Object.keys(state.authentication_providers ?? {})) {
const prov = state.authentication_providers![provUrl];
if ("error_code" in prov) {
continue;
}
if (!("http_status" in prov && prov.http_status === 200)) {
continue;
}
const methodCost: Record<string, AmountString> = {};
for (const meth of prov.methods) {
methodCost[meth.type] = meth.usage_fee;
}
providers.push({
methodCost,
url: provUrl,
});
}
const pol = suggestPolicies(methods, providers);
console.log("policies", pol);
return {
...state,
backup_state: BackupStates.PoliciesReviewing,
...pol,
};
} else {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: `Unsupported action '${action}'`,
};
}
}
if (state.backup_state === BackupStates.PoliciesReviewing) {
if (action === "back") {
return {
...state,
backup_state: BackupStates.AuthenticationsEditing,
};
} else if (action === "delete_policy") {
const ta = args as ActionArgDeletePolicy;
const policies = [...(state.policies ?? [])];
policies.splice(ta.policy_index, 1);
return {
...state,
policies,
};
} else if (action === "next") {
return {
...state,
backup_state: BackupStates.SecretEditing,
};
} else {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: `Unsupported action '${action}'`,
};
}
}
if (state.backup_state === BackupStates.SecretEditing) {
if (action === "back") {
return {
...state,
backup_state: BackupStates.PoliciesReviewing,
};
} else if (action === "enter_secret_name") {
const ta = args as ActionArgEnterSecretName;
return {
...state,
secret_name: ta.name,
};
} else if (action === "enter_secret") {
const ta = args as ActionArgEnterSecret;
return {
...state,
expiration: ta.expiration,
core_secret: {
mime: ta.secret.mime ?? "text/plain",
value: ta.secret.value,
},
};
} else if (action === "next") {
return uploadSecret(state);
} else {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: `Unsupported action '${action}'`,
};
}
}
return {
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: "Reducer action invalid",
};
}

View File

@ -0,0 +1,74 @@
import { AmountString } from "@gnu-taler/taler-util";
export interface EscrowConfigurationResponse {
// Protocol identifier, clarifies that this is an Anastasis provider.
name: "anastasis";
// libtool-style representation of the Exchange protocol version, see
// https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
// The format is "current:revision:age".
version: string;
// Currency in which this provider processes payments.
currency: string;
// Supported authorization methods.
methods: AuthorizationMethodConfig[];
// Maximum policy upload size supported.
storage_limit_in_megabytes: number;
// Payment required to maintain an account to store policy documents for a year.
// Users can pay more, in which case the storage time will go up proportionally.
annual_fee: AmountString;
// Payment required to upload truth. To be paid per upload.
truth_upload_fee: AmountString;
// Limit on the liability that the provider is offering with
// respect to the services provided.
liability_limit: AmountString;
// Salt value with 128 bits of entropy.
// Different providers
// will use different high-entropy salt values. The resulting
// **provider salt** is then used in various operations to ensure
// cryptographic operations differ by provider. A provider must
// never change its salt value.
server_salt: string;
business_name: string;
}
export interface AuthorizationMethodConfig {
// Name of the authorization method.
type: string;
// Fee for accessing key share using this method.
cost: AmountString;
}
export interface TruthUploadRequest {
// Contains the information of an interface EncryptedKeyShare, but simply
// as one binary block (in Crockford Base32 encoding for JSON).
key_share_data: string;
// Key share method, i.e. "security question", "SMS", "e-mail", ...
type: string;
// Variable-size truth. After decryption,
// this contains the ground truth, i.e. H(challenge answer),
// phone number, e-mail address, picture, fingerprint, ...
// **base32 encoded**.
//
// The nonce of the HKDF for this encryption must include the
// string "ECT".
encrypted_truth: string; //bytearray
// MIME type of truth, i.e. text/ascii, image/jpeg, etc.
truth_mime?: string;
// For how many years from now would the client like us to
// store the truth?
storage_duration_years: number;
}

View File

@ -0,0 +1,241 @@
import { Duration } from "@gnu-taler/taler-util";
export type ReducerState =
| ReducerStateBackup
| ReducerStateRecovery
| ReducerStateError;
export interface ContinentInfo {
name: string;
}
export interface CountryInfo {
code: string;
name: string;
continent: string;
currency: string;
}
export interface Policy {
methods: {
authentication_method: number;
provider: string;
}[];
}
export interface PolicyProvider {
provider_url: string;
}
export interface ReducerStateBackup {
recovery_state?: undefined;
backup_state: BackupStates;
code?: undefined;
currencies?: string[];
continents?: ContinentInfo[];
countries?: any;
identity_attributes?: { [n: string]: string };
authentication_providers?: { [url: string]: AuthenticationProviderStatus };
authentication_methods?: AuthMethod[];
required_attributes?: any;
selected_continent?: string;
selected_country?: string;
secret_name?: string;
policies?: Policy[];
/**
* Policy providers are providers that we checked to be functional
* and that are actually used in policies.
*/
policy_providers?: PolicyProvider[];
success_details?: {
[provider_url: string]: {
policy_version: number;
};
};
payments?: string[];
policy_payment_requests?: {
payto: string;
provider: string;
}[];
core_secret?: {
mime: string;
value: string;
};
expiration?: Duration;
}
export interface AuthMethod {
type: string;
instructions: string;
challenge: string;
mime_type?: string;
}
export interface ChallengeInfo {
cost: string;
instructions: string;
type: string;
uuid: string;
}
export interface UserAttributeSpec {
label: string;
name: string;
type: string;
uuid: string;
widget: string;
}
export interface ReducerStateRecovery {
backup_state?: undefined;
recovery_state: RecoveryStates;
code?: undefined;
identity_attributes?: { [n: string]: string };
continents?: any;
countries?: any;
required_attributes?: any;
recovery_information?: {
challenges: ChallengeInfo[];
policies: {
/**
* UUID of the associated challenge.
*/
uuid: string;
}[][];
};
recovery_document?: {
secret_name: string;
provider_url: string;
version: number;
};
selected_challenge_uuid?: string;
challenge_feedback?: { [uuid: string]: ChallengeFeedback };
core_secret?: {
mime: string;
value: string;
};
authentication_providers?: {
[url: string]: {
business_name: string;
};
};
recovery_error?: any;
}
export interface ChallengeFeedback {
state: string;
}
export interface ReducerStateError {
backup_state?: undefined;
recovery_state?: undefined;
code: number;
hint?: string;
message?: string;
}
export enum BackupStates {
ContinentSelecting = "CONTINENT_SELECTING",
CountrySelecting = "COUNTRY_SELECTING",
UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING",
AuthenticationsEditing = "AUTHENTICATIONS_EDITING",
PoliciesReviewing = "POLICIES_REVIEWING",
SecretEditing = "SECRET_EDITING",
TruthsPaying = "TRUTHS_PAYING",
PoliciesPaying = "POLICIES_PAYING",
BackupFinished = "BACKUP_FINISHED",
}
export enum RecoveryStates {
ContinentSelecting = "CONTINENT_SELECTING",
CountrySelecting = "COUNTRY_SELECTING",
UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING",
SecretSelecting = "SECRET_SELECTING",
ChallengeSelecting = "CHALLENGE_SELECTING",
ChallengePaying = "CHALLENGE_PAYING",
ChallengeSolving = "CHALLENGE_SOLVING",
RecoveryFinished = "RECOVERY_FINISHED",
}
export interface MethodSpec {
type: string;
usage_fee: string;
}
// FIXME: This should be tagged!
export type AuthenticationProviderStatusEmpty = {};
export interface AuthenticationProviderStatusOk {
annual_fee: string;
business_name: string;
currency: string;
http_status: 200;
liability_limit: string;
salt: string;
storage_limit_in_megabytes: number;
truth_upload_fee: string;
methods: MethodSpec[];
}
export interface AuthenticationProviderStatusError {
http_status: number;
error_code: number;
}
export type AuthenticationProviderStatus =
| AuthenticationProviderStatusEmpty
| AuthenticationProviderStatusError
| AuthenticationProviderStatusOk;
export interface ReducerStateBackupUserAttributesCollecting
extends ReducerStateBackup {
backup_state: BackupStates.UserAttributesCollecting;
selected_country: string;
currencies: string[];
required_attributes: UserAttributeSpec[];
authentication_providers: { [url: string]: AuthenticationProviderStatus };
}
export interface ActionArgEnterUserAttributes {
identity_attributes: Record<string, string>;
}
export interface ActionArgAddAuthentication {
authentication_method: {
type: string;
instructions: string;
challenge: string;
mime?: string;
};
}
export interface ActionArgDeleteAuthentication {
authentication_method: number;
}
export interface ActionArgDeletePolicy {
policy_index: number;
}
export interface ActionArgEnterSecretName {
name: string;
}
export interface ActionArgEnterSecret {
secret: {
value: string;
mime?: string;
};
expiration: Duration;
}

View File

@ -6,7 +6,7 @@
"module": "ESNext",
"moduleResolution": "node",
"sourceMap": true,
"lib": ["es6"],
"lib": ["es6", "DOM"],
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strict": true,