anastasis: discovery
This commit is contained in:
parent
afecab8000
commit
1e92093a50
@ -1,16 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
bytesToString,
|
|
||||||
canonicalJson,
|
canonicalJson,
|
||||||
decodeCrock,
|
decodeCrock,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
kdf,
|
|
||||||
kdfKw,
|
kdfKw,
|
||||||
secretbox,
|
secretbox,
|
||||||
crypto_sign_keyPair_fromSeed,
|
crypto_sign_keyPair_fromSeed,
|
||||||
stringToBytes,
|
stringToBytes,
|
||||||
secretbox_open,
|
secretbox_open,
|
||||||
hash,
|
hash,
|
||||||
|
bytesToString,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { argon2id } from "hash-wasm";
|
import { argon2id } from "hash-wasm";
|
||||||
|
|
||||||
@ -111,6 +110,42 @@ export async function decryptRecoveryDocument(
|
|||||||
return anastasisDecrypt(asOpaque(userId), recoveryDocData, "erd");
|
return anastasisDecrypt(asOpaque(userId), recoveryDocData, "erd");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PolicyMetadata {
|
||||||
|
secret_name: string;
|
||||||
|
policy_hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function encryptPolicyMetadata(
|
||||||
|
userId: UserIdentifier,
|
||||||
|
metadata: PolicyMetadata,
|
||||||
|
): Promise<OpaqueData> {
|
||||||
|
const metadataBytes = typedArrayConcat([
|
||||||
|
decodeCrock(metadata.policy_hash),
|
||||||
|
stringToBytes(metadata.secret_name),
|
||||||
|
]);
|
||||||
|
const nonce = encodeCrock(getRandomBytes(nonceSize));
|
||||||
|
return anastasisEncrypt(
|
||||||
|
nonce,
|
||||||
|
asOpaque(userId),
|
||||||
|
encodeCrock(metadataBytes),
|
||||||
|
"rmd",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decryptPolicyMetadata(
|
||||||
|
userId: UserIdentifier,
|
||||||
|
metadataEnc: OpaqueData,
|
||||||
|
): Promise<PolicyMetadata> {
|
||||||
|
const plain = await anastasisDecrypt(asOpaque(userId), metadataEnc, "rmd");
|
||||||
|
const metadataBytes = decodeCrock(plain);
|
||||||
|
const policyHash = encodeCrock(metadataBytes.slice(0, 64));
|
||||||
|
const secretName = bytesToString(metadataBytes.slice(64));
|
||||||
|
return {
|
||||||
|
policy_hash: policyHash,
|
||||||
|
secret_name: secretName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function typedArrayConcat(chunks: Uint8Array[]): Uint8Array {
|
export function typedArrayConcat(chunks: Uint8Array[]): Uint8Array {
|
||||||
let payloadLen = 0;
|
let payloadLen = 0;
|
||||||
for (const c of chunks) {
|
for (const c of chunks) {
|
||||||
|
@ -22,11 +22,13 @@ import {
|
|||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
TalerSignaturePurpose,
|
TalerSignaturePurpose,
|
||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
|
URL,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { anastasisData } from "./anastasis-data.js";
|
import { anastasisData } from "./anastasis-data.js";
|
||||||
import {
|
import {
|
||||||
EscrowConfigurationResponse,
|
EscrowConfigurationResponse,
|
||||||
IbanExternalAuthResponse,
|
IbanExternalAuthResponse,
|
||||||
|
RecoveryMetaResponse as RecoveryMetaResponse,
|
||||||
TruthUploadRequest,
|
TruthUploadRequest,
|
||||||
} from "./provider-types.js";
|
} from "./provider-types.js";
|
||||||
import {
|
import {
|
||||||
@ -68,6 +70,10 @@ import {
|
|||||||
ActionArgsUpdatePolicy,
|
ActionArgsUpdatePolicy,
|
||||||
ActionArgsAddProvider,
|
ActionArgsAddProvider,
|
||||||
ActionArgsDeleteProvider,
|
ActionArgsDeleteProvider,
|
||||||
|
DiscoveryCursor,
|
||||||
|
DiscoveryResult,
|
||||||
|
PolicyMetaInfo,
|
||||||
|
ChallengeInfo,
|
||||||
} from "./reducer-types.js";
|
} from "./reducer-types.js";
|
||||||
import fetchPonyfill from "fetch-ponyfill";
|
import fetchPonyfill from "fetch-ponyfill";
|
||||||
import {
|
import {
|
||||||
@ -91,6 +97,8 @@ import {
|
|||||||
KeyShare,
|
KeyShare,
|
||||||
coreSecretRecover,
|
coreSecretRecover,
|
||||||
pinAnswerHash,
|
pinAnswerHash,
|
||||||
|
decryptPolicyMetadata,
|
||||||
|
encryptPolicyMetadata,
|
||||||
} from "./crypto.js";
|
} from "./crypto.js";
|
||||||
import { unzlibSync, zlibSync } from "fflate";
|
import { unzlibSync, zlibSync } from "fflate";
|
||||||
import {
|
import {
|
||||||
@ -112,6 +120,8 @@ export * from "./challenge-feedback-types.js";
|
|||||||
|
|
||||||
const logger = new Logger("anastasis-core:index.ts");
|
const logger = new Logger("anastasis-core:index.ts");
|
||||||
|
|
||||||
|
const ANASTASIS_HTTP_HEADER_POLICY_META_DATA = "Anastasis-Policy-Meta-Data";
|
||||||
|
|
||||||
function getContinents(
|
function getContinents(
|
||||||
opts: { requireProvider?: boolean } = {},
|
opts: { requireProvider?: boolean } = {},
|
||||||
): ContinentInfo[] {
|
): ContinentInfo[] {
|
||||||
@ -224,10 +234,12 @@ async function selectCountry(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const providers: { [x: string]: {} } = {};
|
const providers: { [x: string]: AuthenticationProviderStatus } = {};
|
||||||
for (const prov of anastasisData.providersList.anastasis_provider) {
|
for (const prov of anastasisData.providersList.anastasis_provider) {
|
||||||
if (currencies.includes(prov.currency)) {
|
if (currencies.includes(prov.currency)) {
|
||||||
providers[prov.url] = {};
|
providers[prov.url] = {
|
||||||
|
status: "not-contacted",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,12 +285,14 @@ async function getProviderInfo(
|
|||||||
resp = await fetch(new URL("config", providerBaseUrl).href);
|
resp = await fetch(new URL("config", providerBaseUrl).href);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
|
status: "error",
|
||||||
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
|
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
|
||||||
hint: "request to provider failed",
|
hint: "request to provider failed",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (resp.status !== 200) {
|
if (resp.status !== 200) {
|
||||||
return {
|
return {
|
||||||
|
status: "error",
|
||||||
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
|
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
|
||||||
hint: "unexpected status",
|
hint: "unexpected status",
|
||||||
http_status: resp.status,
|
http_status: resp.status,
|
||||||
@ -287,6 +301,7 @@ async function getProviderInfo(
|
|||||||
try {
|
try {
|
||||||
const jsonResp: EscrowConfigurationResponse = await resp.json();
|
const jsonResp: EscrowConfigurationResponse = await resp.json();
|
||||||
return {
|
return {
|
||||||
|
status: "ok",
|
||||||
http_status: 200,
|
http_status: 200,
|
||||||
annual_fee: jsonResp.annual_fee,
|
annual_fee: jsonResp.annual_fee,
|
||||||
business_name: jsonResp.business_name,
|
business_name: jsonResp.business_name,
|
||||||
@ -299,9 +314,10 @@ async function getProviderInfo(
|
|||||||
salt: jsonResp.server_salt,
|
salt: jsonResp.server_salt,
|
||||||
storage_limit_in_megabytes: jsonResp.storage_limit_in_megabytes,
|
storage_limit_in_megabytes: jsonResp.storage_limit_in_megabytes,
|
||||||
truth_upload_fee: jsonResp.truth_upload_fee,
|
truth_upload_fee: jsonResp.truth_upload_fee,
|
||||||
} as AuthenticationProviderStatusOk;
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
|
status: "error",
|
||||||
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
|
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
|
||||||
hint: "provider did not return JSON",
|
hint: "provider did not return JSON",
|
||||||
};
|
};
|
||||||
@ -594,6 +610,7 @@ async function uploadSecret(
|
|||||||
const userId = await getUserIdCaching(prov.provider_url);
|
const userId = await getUserIdCaching(prov.provider_url);
|
||||||
const acctKeypair = accountKeypairDerive(userId);
|
const acctKeypair = accountKeypairDerive(userId);
|
||||||
const zippedDoc = await compressRecoveryDoc(rd);
|
const zippedDoc = await compressRecoveryDoc(rd);
|
||||||
|
const recoveryDocHash = encodeCrock(hash(zippedDoc));
|
||||||
const encRecoveryDoc = await encryptRecoveryDocument(
|
const encRecoveryDoc = await encryptRecoveryDocument(
|
||||||
userId,
|
userId,
|
||||||
encodeCrock(zippedDoc),
|
encodeCrock(zippedDoc),
|
||||||
@ -603,6 +620,10 @@ async function uploadSecret(
|
|||||||
.put(bodyHash)
|
.put(bodyHash)
|
||||||
.build();
|
.build();
|
||||||
const sig = eddsaSign(sigPS, decodeCrock(acctKeypair.priv));
|
const sig = eddsaSign(sigPS, decodeCrock(acctKeypair.priv));
|
||||||
|
const metadataEnc = await encryptPolicyMetadata(userId, {
|
||||||
|
policy_hash: recoveryDocHash,
|
||||||
|
secret_name: state.secret_name ?? "<unnamed secret>",
|
||||||
|
});
|
||||||
const talerPayUri = state.policy_payment_requests?.find(
|
const talerPayUri = state.policy_payment_requests?.find(
|
||||||
(x) => x.provider === prov.provider_url,
|
(x) => x.provider === prov.provider_url,
|
||||||
)?.payto;
|
)?.payto;
|
||||||
@ -621,6 +642,7 @@ async function uploadSecret(
|
|||||||
headers: {
|
headers: {
|
||||||
"Anastasis-Policy-Signature": encodeCrock(sig),
|
"Anastasis-Policy-Signature": encodeCrock(sig),
|
||||||
"If-None-Match": encodeCrock(bodyHash),
|
"If-None-Match": encodeCrock(bodyHash),
|
||||||
|
[ANASTASIS_HTTP_HEADER_POLICY_META_DATA]: metadataEnc,
|
||||||
...(paySecret
|
...(paySecret
|
||||||
? {
|
? {
|
||||||
"Anastasis-Payment-Identifier": paySecret,
|
"Anastasis-Payment-Identifier": paySecret,
|
||||||
@ -704,37 +726,21 @@ async function uploadSecret(
|
|||||||
async function downloadPolicy(
|
async function downloadPolicy(
|
||||||
state: ReducerStateRecovery,
|
state: ReducerStateRecovery,
|
||||||
): Promise<ReducerStateRecovery | ReducerStateError> {
|
): Promise<ReducerStateRecovery | ReducerStateError> {
|
||||||
const providerUrls = Object.keys(state.authentication_providers ?? {});
|
|
||||||
let foundRecoveryInfo: RecoveryInternalData | undefined = undefined;
|
let foundRecoveryInfo: RecoveryInternalData | undefined = undefined;
|
||||||
let recoveryDoc: RecoveryDocument | undefined = undefined;
|
let recoveryDoc: RecoveryDocument | undefined = undefined;
|
||||||
const newProviderStatus: { [url: string]: AuthenticationProviderStatusOk } =
|
|
||||||
{};
|
|
||||||
const userAttributes = state.identity_attributes!;
|
const userAttributes = state.identity_attributes!;
|
||||||
const restrictProvider = state.selected_provider_url;
|
if (!state.selected_version) {
|
||||||
// FIXME: Shouldn't we also store the status of bad providers?
|
throw Error("invalid state");
|
||||||
for (const url of providerUrls) {
|
|
||||||
const pi = await getProviderInfo(url);
|
|
||||||
if ("error_code" in pi || !("http_status" in pi)) {
|
|
||||||
// Could not even get /config of the provider
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
newProviderStatus[url] = pi;
|
|
||||||
}
|
}
|
||||||
for (const url of providerUrls) {
|
for (const prov of state.selected_version.providers) {
|
||||||
const pi = newProviderStatus[url];
|
const pi = state.authentication_providers?.[prov.provider_url];
|
||||||
if (!pi) {
|
if (!pi || pi.status !== "ok") {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (restrictProvider && url !== state.selected_provider_url) {
|
|
||||||
// User wants specific provider.
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const userId = await userIdentifierDerive(userAttributes, pi.salt);
|
const userId = await userIdentifierDerive(userAttributes, pi.salt);
|
||||||
const acctKeypair = accountKeypairDerive(userId);
|
const acctKeypair = accountKeypairDerive(userId);
|
||||||
const reqUrl = new URL(`policy/${acctKeypair.pub}`, url);
|
const reqUrl = new URL(`policy/${acctKeypair.pub}`, prov.provider_url);
|
||||||
if (state.selected_version) {
|
reqUrl.searchParams.set("version", `${prov.version}`);
|
||||||
reqUrl.searchParams.set("version", `${state.selected_version}`);
|
|
||||||
}
|
|
||||||
const resp = await fetch(reqUrl.href);
|
const resp = await fetch(reqUrl.href);
|
||||||
if (resp.status !== 200) {
|
if (resp.status !== 200) {
|
||||||
continue;
|
continue;
|
||||||
@ -752,7 +758,7 @@ async function downloadPolicy(
|
|||||||
policyVersion = Number(resp.headers.get("Anastasis-Version") ?? "0");
|
policyVersion = Number(resp.headers.get("Anastasis-Version") ?? "0");
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
foundRecoveryInfo = {
|
foundRecoveryInfo = {
|
||||||
provider_url: url,
|
provider_url: prov.provider_url,
|
||||||
secret_name: rd.secret_name ?? "<unknown>",
|
secret_name: rd.secret_name ?? "<unknown>",
|
||||||
version: policyVersion,
|
version: policyVersion,
|
||||||
};
|
};
|
||||||
@ -765,16 +771,24 @@ async function downloadPolicy(
|
|||||||
hint: "No backups found at any provider for your identity information.",
|
hint: "No backups found at any provider for your identity information.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const challenges: ChallengeInfo[] = [];
|
||||||
|
|
||||||
|
for (const x of recoveryDoc.escrow_methods) {
|
||||||
|
const pi = state.authentication_providers?.[x.url];
|
||||||
|
if (!pi || pi.status !== "ok") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
challenges.push({
|
||||||
|
cost: pi.methods.find((m) => m.type === x.escrow_type)?.usage_fee!,
|
||||||
|
instructions: x.instructions,
|
||||||
|
type: x.escrow_type,
|
||||||
|
uuid: x.uuid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const recoveryInfo: RecoveryInformation = {
|
const recoveryInfo: RecoveryInformation = {
|
||||||
challenges: recoveryDoc.escrow_methods.map((x) => {
|
challenges,
|
||||||
const prov = newProviderStatus[x.url] as AuthenticationProviderStatusOk;
|
|
||||||
return {
|
|
||||||
cost: prov.methods.find((m) => m.type === x.escrow_type)?.usage_fee!,
|
|
||||||
instructions: x.instructions,
|
|
||||||
type: x.escrow_type,
|
|
||||||
uuid: x.uuid,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
policies: recoveryDoc.policies.map((x) => {
|
policies: recoveryDoc.policies.map((x) => {
|
||||||
return x.uuids.map((m) => {
|
return x.uuids.map((m) => {
|
||||||
return {
|
return {
|
||||||
@ -785,7 +799,7 @@ async function downloadPolicy(
|
|||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
recovery_state: RecoveryStates.SecretSelecting,
|
recovery_state: RecoveryStates.ChallengeSelecting,
|
||||||
recovery_document: foundRecoveryInfo,
|
recovery_document: foundRecoveryInfo,
|
||||||
recovery_information: recoveryInfo,
|
recovery_information: recoveryInfo,
|
||||||
verbatim_recovery_document: recoveryDoc,
|
verbatim_recovery_document: recoveryDoc,
|
||||||
@ -1019,10 +1033,11 @@ async function recoveryEnterUserAttributes(
|
|||||||
}
|
}
|
||||||
const st: ReducerStateRecovery = {
|
const st: ReducerStateRecovery = {
|
||||||
...state,
|
...state,
|
||||||
|
recovery_state: RecoveryStates.SecretSelecting,
|
||||||
identity_attributes: args.identity_attributes,
|
identity_attributes: args.identity_attributes,
|
||||||
authentication_providers: newProviders,
|
authentication_providers: newProviders,
|
||||||
};
|
};
|
||||||
return downloadPolicy(st);
|
return st;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeVersion(
|
async function changeVersion(
|
||||||
@ -1031,8 +1046,7 @@ async function changeVersion(
|
|||||||
): Promise<ReducerStateRecovery | ReducerStateError> {
|
): Promise<ReducerStateRecovery | ReducerStateError> {
|
||||||
const st: ReducerStateRecovery = {
|
const st: ReducerStateRecovery = {
|
||||||
...state,
|
...state,
|
||||||
selected_version: args.version,
|
selected_version: args.selection,
|
||||||
selected_provider_url: args.provider_url,
|
|
||||||
};
|
};
|
||||||
return downloadPolicy(st);
|
return downloadPolicy(st);
|
||||||
}
|
}
|
||||||
@ -1313,10 +1327,7 @@ async function nextFromAuthenticationsEditing(
|
|||||||
const providers: ProviderInfo[] = [];
|
const providers: ProviderInfo[] = [];
|
||||||
for (const provUrl of Object.keys(state.authentication_providers ?? {})) {
|
for (const provUrl of Object.keys(state.authentication_providers ?? {})) {
|
||||||
const prov = state.authentication_providers![provUrl];
|
const prov = state.authentication_providers![provUrl];
|
||||||
if ("error_code" in prov) {
|
if (prov.status !== "ok") {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!("http_status" in prov && prov.http_status === 200)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const methodCost: Record<string, AmountString> = {};
|
const methodCost: Record<string, AmountString> = {};
|
||||||
@ -1574,6 +1585,59 @@ const recoveryTransitions: Record<
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function discoverPolicies(
|
||||||
|
state: ReducerState,
|
||||||
|
cursor?: DiscoveryCursor,
|
||||||
|
): Promise<DiscoveryResult> {
|
||||||
|
if (!state.recovery_state) {
|
||||||
|
throw Error("can only discover providers in recovery state");
|
||||||
|
}
|
||||||
|
|
||||||
|
const policies: PolicyMetaInfo[] = [];
|
||||||
|
|
||||||
|
const providerUrls = Object.keys(state.authentication_providers || {});
|
||||||
|
// FIXME: Do we need to re-contact providers here / check if they're disabled?
|
||||||
|
|
||||||
|
for (const providerUrl of providerUrls) {
|
||||||
|
const providerInfo = await getProviderInfo(providerUrl);
|
||||||
|
if (providerInfo.status !== "ok") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const userId = await userIdentifierDerive(
|
||||||
|
state.identity_attributes!,
|
||||||
|
providerInfo.salt,
|
||||||
|
);
|
||||||
|
const acctKeypair = accountKeypairDerive(userId);
|
||||||
|
const reqUrl = new URL(`policy/${acctKeypair.pub}/meta`, providerUrl);
|
||||||
|
const resp = await fetch(reqUrl.href);
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
logger.warn(`Could not fetch policy metadate from ${reqUrl.href}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const respJson: RecoveryMetaResponse = await resp.json();
|
||||||
|
const versions = Object.keys(respJson);
|
||||||
|
for (const version of versions) {
|
||||||
|
const item = respJson[version];
|
||||||
|
if (!item.meta) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const metaData = await decryptPolicyMetadata(userId, item.meta!);
|
||||||
|
policies.push({
|
||||||
|
attribute_mask: 0,
|
||||||
|
provider_url: providerUrl,
|
||||||
|
server_time: item.upload_time,
|
||||||
|
version: Number.parseInt(version, 10),
|
||||||
|
secret_name: metaData.secret_name,
|
||||||
|
policy_hash: metaData.policy_hash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
policies,
|
||||||
|
cursor: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function reduceAction(
|
export async function reduceAction(
|
||||||
state: ReducerState,
|
state: ReducerState,
|
||||||
action: string,
|
action: string,
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { Amounts, AmountString } from "@gnu-taler/taler-util";
|
import {
|
||||||
|
Amounts,
|
||||||
|
AmountString,
|
||||||
|
TalerProtocolDuration,
|
||||||
|
TalerProtocolTimestamp,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
export interface EscrowConfigurationResponse {
|
export interface EscrowConfigurationResponse {
|
||||||
// Protocol identifier, clarifies that this is an Anastasis provider.
|
// Protocol identifier, clarifies that this is an Anastasis provider.
|
||||||
@ -83,3 +88,20 @@ export interface IbanExternalAuthResponse {
|
|||||||
wire_transfer_subject: string;
|
wire_transfer_subject: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RecoveryMetaResponse {
|
||||||
|
/**
|
||||||
|
* Version numbers as a string (!) are used as keys.
|
||||||
|
*/
|
||||||
|
[version: string]: RecoveryMetaDataItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecoveryMetaDataItem {
|
||||||
|
// The meta value can be NULL if the document
|
||||||
|
// exists but no meta data was provided.
|
||||||
|
meta?: string;
|
||||||
|
|
||||||
|
// Server-time indicative of when the recovery
|
||||||
|
// document was uploaded.
|
||||||
|
upload_time: TalerProtocolTimestamp;
|
||||||
|
}
|
||||||
|
@ -202,14 +202,9 @@ export interface ReducerStateRecovery {
|
|||||||
/**
|
/**
|
||||||
* Explicitly selected version by the user.
|
* Explicitly selected version by the user.
|
||||||
* FIXME: In the C reducer this is called "version".
|
* FIXME: In the C reducer this is called "version".
|
||||||
|
* FIXME: rename to selected_secret / selected_policy?
|
||||||
*/
|
*/
|
||||||
selected_version?: number;
|
selected_version?: AggregatedPolicyMetaInfo;
|
||||||
|
|
||||||
/**
|
|
||||||
* Explicitly selected provider URL by the user.
|
|
||||||
* FIXME: In the C reducer this is called "provider_url".
|
|
||||||
*/
|
|
||||||
selected_provider_url?: string;
|
|
||||||
|
|
||||||
challenge_feedback?: { [uuid: string]: ChallengeFeedback };
|
challenge_feedback?: { [uuid: string]: ChallengeFeedback };
|
||||||
|
|
||||||
@ -291,10 +286,12 @@ export interface MethodSpec {
|
|||||||
usage_fee: string;
|
usage_fee: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: This should be tagged!
|
export type AuthenticationProviderStatusEmpty = {
|
||||||
export type AuthenticationProviderStatusEmpty = {};
|
status: "not-contacted";
|
||||||
|
};
|
||||||
|
|
||||||
export interface AuthenticationProviderStatusOk {
|
export interface AuthenticationProviderStatusOk {
|
||||||
|
status: "ok";
|
||||||
annual_fee: string;
|
annual_fee: string;
|
||||||
business_name: string;
|
business_name: string;
|
||||||
currency: string;
|
currency: string;
|
||||||
@ -304,11 +301,15 @@ export interface AuthenticationProviderStatusOk {
|
|||||||
storage_limit_in_megabytes: number;
|
storage_limit_in_megabytes: number;
|
||||||
truth_upload_fee: string;
|
truth_upload_fee: string;
|
||||||
methods: MethodSpec[];
|
methods: MethodSpec[];
|
||||||
|
// FIXME: add timestamp?
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthenticationProviderStatusError {
|
export interface AuthenticationProviderStatusError {
|
||||||
http_status: number;
|
status: "error";
|
||||||
error_code: number;
|
http_status?: number;
|
||||||
|
code: number;
|
||||||
|
hint?: string;
|
||||||
|
// FIXME: add timestamp?
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthenticationProviderStatus =
|
export type AuthenticationProviderStatus =
|
||||||
@ -441,8 +442,7 @@ export interface ActionArgsUpdateExpiration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionArgsChangeVersion {
|
export interface ActionArgsChangeVersion {
|
||||||
provider_url: string;
|
selection: AggregatedPolicyMetaInfo;
|
||||||
version: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionArgsUpdatePolicy {
|
export interface ActionArgsUpdatePolicy {
|
||||||
@ -450,10 +450,55 @@ export interface ActionArgsUpdatePolicy {
|
|||||||
policy: PolicyMember[];
|
policy: PolicyMember[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor for a provider discovery process.
|
||||||
|
*/
|
||||||
|
export interface DiscoveryCursor {
|
||||||
|
position: {
|
||||||
|
provider_url: string;
|
||||||
|
mask: number;
|
||||||
|
max_version?: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PolicyMetaInfo {
|
||||||
|
policy_hash: string;
|
||||||
|
provider_url: string;
|
||||||
|
version: number;
|
||||||
|
attribute_mask: number;
|
||||||
|
server_time: TalerProtocolTimestamp;
|
||||||
|
secret_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregated / de-duplicated policy meta info.
|
||||||
|
*/
|
||||||
|
export interface AggregatedPolicyMetaInfo {
|
||||||
|
secret_name?: string;
|
||||||
|
policy_hash: string;
|
||||||
|
attribute_mask: number;
|
||||||
|
providers: {
|
||||||
|
provider_url: string;
|
||||||
|
version: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiscoveryResult {
|
||||||
|
/**
|
||||||
|
* Found policies.
|
||||||
|
*/
|
||||||
|
policies: PolicyMetaInfo[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor that allows getting more results.
|
||||||
|
*/
|
||||||
|
cursor?: DiscoveryCursor;
|
||||||
|
}
|
||||||
|
|
||||||
export const codecForActionArgsChangeVersion = () =>
|
export const codecForActionArgsChangeVersion = () =>
|
||||||
buildCodecForObject<ActionArgsChangeVersion>()
|
buildCodecForObject<ActionArgsChangeVersion>()
|
||||||
.property("provider_url", codecForString())
|
.property("selection", codecForAny())
|
||||||
.property("version", codecForNumber())
|
|
||||||
.build("ActionArgsChangeVersion");
|
.build("ActionArgsChangeVersion");
|
||||||
|
|
||||||
export const codecForPolicyMember = () =>
|
export const codecForPolicyMember = () =>
|
||||||
|
@ -46,7 +46,7 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode {
|
|||||||
Contact us
|
Contact us
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://bugs.anastasis.li/"
|
href="https://bugs.anastasis.lu/"
|
||||||
style={{ alignSelf: "center", padding: "0.5em" }}
|
style={{ alignSelf: "center", padding: "0.5em" }}
|
||||||
>
|
>
|
||||||
Report a bug
|
Report a bug
|
||||||
|
@ -23,11 +23,9 @@ import { createContext, h, VNode } from "preact";
|
|||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
import { AnastasisReducerApi } from "../hooks/use-anastasis-reducer";
|
import { AnastasisReducerApi } from "../hooks/use-anastasis-reducer";
|
||||||
|
|
||||||
type Type = AnastasisReducerApi | undefined;
|
|
||||||
|
|
||||||
const initial = undefined;
|
const initial = undefined;
|
||||||
|
|
||||||
const Context = createContext<Type>(initial);
|
const Context = createContext<AnastasisReducerApi | undefined>(initial);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: AnastasisReducerApi;
|
value: AnastasisReducerApi;
|
||||||
@ -38,4 +36,5 @@ export const AnastasisProvider = ({ value, children }: Props): VNode => {
|
|||||||
return h(Context.Provider, { value, children });
|
return h(Context.Provider, { value, children });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAnastasisContext = (): Type => useContext(Context);
|
export const useAnastasisContext = (): AnastasisReducerApi | undefined =>
|
||||||
|
useContext(Context);
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import { TalerErrorCode } from "@gnu-taler/taler-util";
|
import { TalerErrorCode } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
|
AggregatedPolicyMetaInfo,
|
||||||
BackupStates,
|
BackupStates,
|
||||||
|
discoverPolicies,
|
||||||
|
DiscoveryCursor,
|
||||||
getBackupStartState,
|
getBackupStartState,
|
||||||
getRecoveryStartState,
|
getRecoveryStartState,
|
||||||
|
PolicyMetaInfo,
|
||||||
RecoveryStates,
|
RecoveryStates,
|
||||||
reduceAction,
|
reduceAction,
|
||||||
ReducerState,
|
ReducerState,
|
||||||
@ -15,6 +19,7 @@ const remoteReducer = false;
|
|||||||
interface AnastasisState {
|
interface AnastasisState {
|
||||||
reducerState: ReducerState | undefined;
|
reducerState: ReducerState | undefined;
|
||||||
currentError: any;
|
currentError: any;
|
||||||
|
discoveryState: DiscoveryUiState;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getBackupStartStateRemote(): Promise<ReducerState> {
|
async function getBackupStartStateRemote(): Promise<ReducerState> {
|
||||||
@ -98,9 +103,21 @@ export interface ReducerTransactionHandle {
|
|||||||
transition(action: string, args: any): Promise<ReducerState>;
|
transition(action: string, args: any): Promise<ReducerState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI-relevant state of the policy discovery process.
|
||||||
|
*/
|
||||||
|
export interface DiscoveryUiState {
|
||||||
|
state: "none" | "active" | "finished";
|
||||||
|
|
||||||
|
aggregatedPolicies?: AggregatedPolicyMetaInfo[];
|
||||||
|
|
||||||
|
cursor?: DiscoveryCursor;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AnastasisReducerApi {
|
export interface AnastasisReducerApi {
|
||||||
currentReducerState: ReducerState | undefined;
|
currentReducerState: ReducerState | undefined;
|
||||||
currentError: any;
|
currentError: any;
|
||||||
|
discoveryState: DiscoveryUiState;
|
||||||
dismissError: () => void;
|
dismissError: () => void;
|
||||||
startBackup: () => void;
|
startBackup: () => void;
|
||||||
startRecover: () => void;
|
startRecover: () => void;
|
||||||
@ -109,6 +126,8 @@ export interface AnastasisReducerApi {
|
|||||||
transition(action: string, args: any): Promise<void>;
|
transition(action: string, args: any): Promise<void>;
|
||||||
exportState: () => string;
|
exportState: () => string;
|
||||||
importState: (s: string) => void;
|
importState: (s: string) => void;
|
||||||
|
discoverStart(): Promise<void>;
|
||||||
|
discoverMore(): Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Run multiple reducer steps in a transaction without
|
* Run multiple reducer steps in a transaction without
|
||||||
* affecting the UI-visible transition state in-between.
|
* affecting the UI-visible transition state in-between.
|
||||||
@ -152,6 +171,9 @@ export function useAnastasisReducer(): AnastasisReducerApi {
|
|||||||
() => ({
|
() => ({
|
||||||
reducerState: getStateFromStorage(),
|
reducerState: getStateFromStorage(),
|
||||||
currentError: undefined,
|
currentError: undefined,
|
||||||
|
discoveryState: {
|
||||||
|
state: "none",
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -192,6 +214,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
|
|||||||
return {
|
return {
|
||||||
currentReducerState: anastasisState.reducerState,
|
currentReducerState: anastasisState.reducerState,
|
||||||
currentError: anastasisState.currentError,
|
currentError: anastasisState.currentError,
|
||||||
|
discoveryState: anastasisState.discoveryState,
|
||||||
async startBackup() {
|
async startBackup() {
|
||||||
let s: ReducerState;
|
let s: ReducerState;
|
||||||
if (remoteReducer) {
|
if (remoteReducer) {
|
||||||
@ -213,17 +236,59 @@ export function useAnastasisReducer(): AnastasisReducerApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
exportState() {
|
exportState() {
|
||||||
const state = getStateFromStorage()
|
const state = getStateFromStorage();
|
||||||
return JSON.stringify(state)
|
return JSON.stringify(state);
|
||||||
},
|
},
|
||||||
importState(s: string) {
|
importState(s: string) {
|
||||||
try {
|
try {
|
||||||
const state = JSON.parse(s)
|
const state = JSON.parse(s);
|
||||||
setAnastasisState({ reducerState: state, currentError: undefined })
|
setAnastasisState({
|
||||||
|
reducerState: state,
|
||||||
|
currentError: undefined,
|
||||||
|
discoveryState: {
|
||||||
|
state: "none",
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Error('could not restore the state')
|
throw Error("could not restore the state");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async discoverStart(): Promise<void> {
|
||||||
|
const res = await discoverPolicies(this.currentReducerState!, undefined);
|
||||||
|
const aggregatedPolicies: AggregatedPolicyMetaInfo[] = [];
|
||||||
|
const polHashToIndex: Record<string, number> = {};
|
||||||
|
for (const pol of res.policies) {
|
||||||
|
const oldIndex = polHashToIndex[pol.policy_hash];
|
||||||
|
if (oldIndex != null) {
|
||||||
|
aggregatedPolicies[oldIndex].providers.push({
|
||||||
|
provider_url: pol.provider_url,
|
||||||
|
version: pol.version,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
aggregatedPolicies.push({
|
||||||
|
attribute_mask: pol.attribute_mask,
|
||||||
|
policy_hash: pol.policy_hash,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provider_url: pol.provider_url,
|
||||||
|
version: pol.version,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
secret_name: pol.secret_name,
|
||||||
|
});
|
||||||
|
polHashToIndex[pol.policy_hash] = aggregatedPolicies.length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setAnastasisState({
|
||||||
|
...anastasisState,
|
||||||
|
discoveryState: {
|
||||||
|
state: "finished",
|
||||||
|
aggregatedPolicies,
|
||||||
|
cursor: res.cursor,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async discoverMore(): Promise<void> {},
|
||||||
async startRecover() {
|
async startRecover() {
|
||||||
let s: ReducerState;
|
let s: ReducerState;
|
||||||
if (remoteReducer) {
|
if (remoteReducer) {
|
||||||
@ -301,7 +366,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ReducerTxImpl implements ReducerTransactionHandle {
|
class ReducerTxImpl implements ReducerTransactionHandle {
|
||||||
constructor(public transactionState: ReducerState) { }
|
constructor(public transactionState: ReducerState) {}
|
||||||
async transition(action: string, args: any): Promise<ReducerState> {
|
async transition(action: string, args: any): Promise<ReducerState> {
|
||||||
let s: ReducerState;
|
let s: ReducerState;
|
||||||
if (remoteReducer) {
|
if (remoteReducer) {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
AuthenticationProviderStatus,
|
AuthenticationProviderStatus,
|
||||||
AuthenticationProviderStatusOk,
|
AuthenticationProviderStatusOk,
|
||||||
|
PolicyMetaInfo,
|
||||||
} from "@gnu-taler/anastasis-core";
|
} from "@gnu-taler/anastasis-core";
|
||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { AsyncButton } from "../../components/AsyncButton";
|
import { AsyncButton } from "../../components/AsyncButton";
|
||||||
import { PhoneNumberInput } from "../../components/fields/NumberInput";
|
import { PhoneNumberInput } from "../../components/fields/NumberInput";
|
||||||
import { useAnastasisContext } from "../../context/anastasis";
|
import { useAnastasisContext } from "../../context/anastasis";
|
||||||
@ -13,8 +14,100 @@ import { AnastasisClientFrame } from "./index";
|
|||||||
export function SecretSelectionScreen(): VNode {
|
export function SecretSelectionScreen(): VNode {
|
||||||
const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
|
const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
|
||||||
const reducer = useAnastasisContext();
|
const reducer = useAnastasisContext();
|
||||||
|
|
||||||
const [manageProvider, setManageProvider] = useState(false);
|
const [manageProvider, setManageProvider] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function f() {
|
||||||
|
if (reducer) {
|
||||||
|
await reducer.discoverStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f().catch((e) => console.log(e));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!reducer) {
|
||||||
|
return <div>no reducer in context</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!reducer.currentReducerState ||
|
||||||
|
reducer.currentReducerState.recovery_state === undefined
|
||||||
|
) {
|
||||||
|
return <div>invalid state</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provs = reducer.currentReducerState.authentication_providers ?? {};
|
||||||
|
const recoveryDocument = reducer.currentReducerState.recovery_document;
|
||||||
|
|
||||||
|
if (manageProvider) {
|
||||||
|
return <AddingProviderScreen onCancel={() => setManageProvider(false)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reducer.discoveryState.state === "none") {
|
||||||
|
// Can this even happen?
|
||||||
|
return (
|
||||||
|
<AnastasisClientFrame title="Recovery: Select secret">
|
||||||
|
<div>waiting to start discovery</div>
|
||||||
|
</AnastasisClientFrame>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reducer.discoveryState.state === "active") {
|
||||||
|
return (
|
||||||
|
<AnastasisClientFrame title="Recovery: Select secret">
|
||||||
|
<div>loading secret versions</div>
|
||||||
|
</AnastasisClientFrame>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const policies = reducer.discoveryState.aggregatedPolicies ?? [];
|
||||||
|
|
||||||
|
if (policies.length === 0) {
|
||||||
|
return (
|
||||||
|
<ChooseAnotherProviderScreen
|
||||||
|
providers={provs}
|
||||||
|
selected=""
|
||||||
|
onChange={(newProv) => () => {}}
|
||||||
|
></ChooseAnotherProviderScreen>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnastasisClientFrame title="Recovery: Select secret" hideNext="Please select version to recover">
|
||||||
|
<p>Found versions:</p>
|
||||||
|
{policies.map((x) => (
|
||||||
|
<div>
|
||||||
|
{x.policy_hash} / {x.secret_name}
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
await reducer.transition("change_version", {
|
||||||
|
selection: x,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Recover
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button>Load older versions</button>
|
||||||
|
</AnastasisClientFrame>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OldSecretSelectionScreen(): VNode {
|
||||||
|
const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
|
||||||
|
const reducer = useAnastasisContext();
|
||||||
|
const [manageProvider, setManageProvider] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function f() {
|
||||||
|
if (reducer) {
|
||||||
|
await reducer.discoverStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f().catch((e) => console.log(e));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const currentVersion =
|
const currentVersion =
|
||||||
(reducer?.currentReducerState &&
|
(reducer?.currentReducerState &&
|
||||||
"recovery_document" in reducer.currentReducerState &&
|
"recovery_document" in reducer.currentReducerState &&
|
||||||
@ -71,15 +164,16 @@ export function SecretSelectionScreen(): VNode {
|
|||||||
return <AddingProviderScreen onCancel={() => setManageProvider(false)} />;
|
return <AddingProviderScreen onCancel={() => setManageProvider(false)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const provierInfo = provs[
|
const providerInfo = provs[
|
||||||
recoveryDocument.provider_url
|
recoveryDocument.provider_url
|
||||||
] as AuthenticationProviderStatusOk;
|
] as AuthenticationProviderStatusOk;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame title="Recovery: Select secret">
|
<AnastasisClientFrame title="Recovery: Select secret">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="box" style={{ border: "2px solid green" }}>
|
<div class="box" style={{ border: "2px solid green" }}>
|
||||||
<h1 class="subtitle">{provierInfo.business_name}</h1>
|
<h1 class="subtitle">{providerInfo.business_name}</h1>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{currentVersion === 0 ? (
|
{currentVersion === 0 ? (
|
||||||
<p>Set to recover the latest version</p>
|
<p>Set to recover the latest version</p>
|
||||||
|
Loading…
Reference in New Issue
Block a user