summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-04-12 12:54:57 +0200
committerFlorian Dold <florian@dold.me>2022-04-12 12:55:32 +0200
commit1e92093a50962f4702339e872caa4f82af90af70 (patch)
tree10d927c7e51e8fd1cf52629dd3d19d3fbd28b1bf /packages
parentafecab8000fa59475fe02a402176789e360651ba (diff)
downloadwallet-core-1e92093a50962f4702339e872caa4f82af90af70.tar.gz
wallet-core-1e92093a50962f4702339e872caa4f82af90af70.tar.bz2
wallet-core-1e92093a50962f4702339e872caa4f82af90af70.zip
anastasis: discovery
Diffstat (limited to 'packages')
-rw-r--r--packages/anastasis-core/src/crypto.ts39
-rw-r--r--packages/anastasis-core/src/index.ts152
-rw-r--r--packages/anastasis-core/src/provider-types.ts24
-rw-r--r--packages/anastasis-core/src/reducer-types.ts75
-rw-r--r--packages/anastasis-webui/src/components/menu/NavigationBar.tsx2
-rw-r--r--packages/anastasis-webui/src/context/anastasis.ts7
-rw-r--r--packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts77
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx100
8 files changed, 400 insertions, 76 deletions
diff --git a/packages/anastasis-core/src/crypto.ts b/packages/anastasis-core/src/crypto.ts
index 75bd4b323..37e8c4f54 100644
--- a/packages/anastasis-core/src/crypto.ts
+++ b/packages/anastasis-core/src/crypto.ts
@@ -1,16 +1,15 @@
import {
- bytesToString,
canonicalJson,
decodeCrock,
encodeCrock,
getRandomBytes,
- kdf,
kdfKw,
secretbox,
crypto_sign_keyPair_fromSeed,
stringToBytes,
secretbox_open,
hash,
+ bytesToString,
} from "@gnu-taler/taler-util";
import { argon2id } from "hash-wasm";
@@ -111,6 +110,42 @@ export async function decryptRecoveryDocument(
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 {
let payloadLen = 0;
for (const c of chunks) {
diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts
index a355eaa54..5a9199e02 100644
--- a/packages/anastasis-core/src/index.ts
+++ b/packages/anastasis-core/src/index.ts
@@ -22,11 +22,13 @@ import {
TalerProtocolTimestamp,
TalerSignaturePurpose,
AbsoluteTime,
+ URL,
} from "@gnu-taler/taler-util";
import { anastasisData } from "./anastasis-data.js";
import {
EscrowConfigurationResponse,
IbanExternalAuthResponse,
+ RecoveryMetaResponse as RecoveryMetaResponse,
TruthUploadRequest,
} from "./provider-types.js";
import {
@@ -68,6 +70,10 @@ import {
ActionArgsUpdatePolicy,
ActionArgsAddProvider,
ActionArgsDeleteProvider,
+ DiscoveryCursor,
+ DiscoveryResult,
+ PolicyMetaInfo,
+ ChallengeInfo,
} from "./reducer-types.js";
import fetchPonyfill from "fetch-ponyfill";
import {
@@ -91,6 +97,8 @@ import {
KeyShare,
coreSecretRecover,
pinAnswerHash,
+ decryptPolicyMetadata,
+ encryptPolicyMetadata,
} from "./crypto.js";
import { unzlibSync, zlibSync } from "fflate";
import {
@@ -112,6 +120,8 @@ export * from "./challenge-feedback-types.js";
const logger = new Logger("anastasis-core:index.ts");
+const ANASTASIS_HTTP_HEADER_POLICY_META_DATA = "Anastasis-Policy-Meta-Data";
+
function getContinents(
opts: { requireProvider?: boolean } = {},
): 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) {
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);
} catch (e) {
return {
+ status: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
hint: "request to provider failed",
};
}
if (resp.status !== 200) {
return {
+ status: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
hint: "unexpected status",
http_status: resp.status,
@@ -287,6 +301,7 @@ async function getProviderInfo(
try {
const jsonResp: EscrowConfigurationResponse = await resp.json();
return {
+ status: "ok",
http_status: 200,
annual_fee: jsonResp.annual_fee,
business_name: jsonResp.business_name,
@@ -299,9 +314,10 @@ async function getProviderInfo(
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 {
+ status: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
hint: "provider did not return JSON",
};
@@ -594,6 +610,7 @@ async function uploadSecret(
const userId = await getUserIdCaching(prov.provider_url);
const acctKeypair = accountKeypairDerive(userId);
const zippedDoc = await compressRecoveryDoc(rd);
+ const recoveryDocHash = encodeCrock(hash(zippedDoc));
const encRecoveryDoc = await encryptRecoveryDocument(
userId,
encodeCrock(zippedDoc),
@@ -603,6 +620,10 @@ async function uploadSecret(
.put(bodyHash)
.build();
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(
(x) => x.provider === prov.provider_url,
)?.payto;
@@ -621,6 +642,7 @@ async function uploadSecret(
headers: {
"Anastasis-Policy-Signature": encodeCrock(sig),
"If-None-Match": encodeCrock(bodyHash),
+ [ANASTASIS_HTTP_HEADER_POLICY_META_DATA]: metadataEnc,
...(paySecret
? {
"Anastasis-Payment-Identifier": paySecret,
@@ -704,37 +726,21 @@ async function uploadSecret(
async function downloadPolicy(
state: ReducerStateRecovery,
): Promise<ReducerStateRecovery | ReducerStateError> {
- const providerUrls = Object.keys(state.authentication_providers ?? {});
let foundRecoveryInfo: RecoveryInternalData | undefined = undefined;
let recoveryDoc: RecoveryDocument | undefined = undefined;
- const newProviderStatus: { [url: string]: AuthenticationProviderStatusOk } =
- {};
const userAttributes = state.identity_attributes!;
- const restrictProvider = state.selected_provider_url;
- // FIXME: Shouldn't we also store the status of bad providers?
- 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;
+ if (!state.selected_version) {
+ throw Error("invalid state");
}
- for (const url of providerUrls) {
- const pi = newProviderStatus[url];
- if (!pi) {
- continue;
- }
- if (restrictProvider && url !== state.selected_provider_url) {
- // User wants specific provider.
+ for (const prov of state.selected_version.providers) {
+ const pi = state.authentication_providers?.[prov.provider_url];
+ if (!pi || pi.status !== "ok") {
continue;
}
const userId = await userIdentifierDerive(userAttributes, pi.salt);
const acctKeypair = accountKeypairDerive(userId);
- const reqUrl = new URL(`policy/${acctKeypair.pub}`, url);
- if (state.selected_version) {
- reqUrl.searchParams.set("version", `${state.selected_version}`);
- }
+ const reqUrl = new URL(`policy/${acctKeypair.pub}`, prov.provider_url);
+ reqUrl.searchParams.set("version", `${prov.version}`);
const resp = await fetch(reqUrl.href);
if (resp.status !== 200) {
continue;
@@ -752,7 +758,7 @@ async function downloadPolicy(
policyVersion = Number(resp.headers.get("Anastasis-Version") ?? "0");
} catch (e) {}
foundRecoveryInfo = {
- provider_url: url,
+ provider_url: prov.provider_url,
secret_name: rd.secret_name ?? "<unknown>",
version: policyVersion,
};
@@ -765,16 +771,24 @@ async function downloadPolicy(
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 = {
- challenges: recoveryDoc.escrow_methods.map((x) => {
- 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,
- };
- }),
+ challenges,
policies: recoveryDoc.policies.map((x) => {
return x.uuids.map((m) => {
return {
@@ -785,7 +799,7 @@ async function downloadPolicy(
};
return {
...state,
- recovery_state: RecoveryStates.SecretSelecting,
+ recovery_state: RecoveryStates.ChallengeSelecting,
recovery_document: foundRecoveryInfo,
recovery_information: recoveryInfo,
verbatim_recovery_document: recoveryDoc,
@@ -1019,10 +1033,11 @@ async function recoveryEnterUserAttributes(
}
const st: ReducerStateRecovery = {
...state,
+ recovery_state: RecoveryStates.SecretSelecting,
identity_attributes: args.identity_attributes,
authentication_providers: newProviders,
};
- return downloadPolicy(st);
+ return st;
}
async function changeVersion(
@@ -1031,8 +1046,7 @@ async function changeVersion(
): Promise<ReducerStateRecovery | ReducerStateError> {
const st: ReducerStateRecovery = {
...state,
- selected_version: args.version,
- selected_provider_url: args.provider_url,
+ selected_version: args.selection,
};
return downloadPolicy(st);
}
@@ -1313,10 +1327,7 @@ async function nextFromAuthenticationsEditing(
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)) {
+ if (prov.status !== "ok") {
continue;
}
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(
state: ReducerState,
action: string,
diff --git a/packages/anastasis-core/src/provider-types.ts b/packages/anastasis-core/src/provider-types.ts
index f4d998e0a..fe6292b02 100644
--- a/packages/anastasis-core/src/provider-types.ts
+++ b/packages/anastasis-core/src/provider-types.ts
@@ -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 {
// Protocol identifier, clarifies that this is an Anastasis provider.
@@ -83,3 +88,20 @@ export interface IbanExternalAuthResponse {
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;
+}
diff --git a/packages/anastasis-core/src/reducer-types.ts b/packages/anastasis-core/src/reducer-types.ts
index 4682eddb7..47238cd3f 100644
--- a/packages/anastasis-core/src/reducer-types.ts
+++ b/packages/anastasis-core/src/reducer-types.ts
@@ -202,14 +202,9 @@ export interface ReducerStateRecovery {
/**
* Explicitly selected version by the user.
* FIXME: In the C reducer this is called "version".
+ * FIXME: rename to selected_secret / selected_policy?
*/
- selected_version?: number;
-
- /**
- * Explicitly selected provider URL by the user.
- * FIXME: In the C reducer this is called "provider_url".
- */
- selected_provider_url?: string;
+ selected_version?: AggregatedPolicyMetaInfo;
challenge_feedback?: { [uuid: string]: ChallengeFeedback };
@@ -291,10 +286,12 @@ export interface MethodSpec {
usage_fee: string;
}
-// FIXME: This should be tagged!
-export type AuthenticationProviderStatusEmpty = {};
+export type AuthenticationProviderStatusEmpty = {
+ status: "not-contacted";
+};
export interface AuthenticationProviderStatusOk {
+ status: "ok";
annual_fee: string;
business_name: string;
currency: string;
@@ -304,11 +301,15 @@ export interface AuthenticationProviderStatusOk {
storage_limit_in_megabytes: number;
truth_upload_fee: string;
methods: MethodSpec[];
+ // FIXME: add timestamp?
}
export interface AuthenticationProviderStatusError {
- http_status: number;
- error_code: number;
+ status: "error";
+ http_status?: number;
+ code: number;
+ hint?: string;
+ // FIXME: add timestamp?
}
export type AuthenticationProviderStatus =
@@ -441,8 +442,7 @@ export interface ActionArgsUpdateExpiration {
}
export interface ActionArgsChangeVersion {
- provider_url: string;
- version: number;
+ selection: AggregatedPolicyMetaInfo;
}
export interface ActionArgsUpdatePolicy {
@@ -450,10 +450,55 @@ export interface ActionArgsUpdatePolicy {
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 = () =>
buildCodecForObject<ActionArgsChangeVersion>()
- .property("provider_url", codecForString())
- .property("version", codecForNumber())
+ .property("selection", codecForAny())
.build("ActionArgsChangeVersion");
export const codecForPolicyMember = () =>
diff --git a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
index 8d5a0473b..bc6d923d7 100644
--- a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
+++ b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
@@ -46,7 +46,7 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode {
Contact us
</a>
<a
- href="https://bugs.anastasis.li/"
+ href="https://bugs.anastasis.lu/"
style={{ alignSelf: "center", padding: "0.5em" }}
>
Report a bug
diff --git a/packages/anastasis-webui/src/context/anastasis.ts b/packages/anastasis-webui/src/context/anastasis.ts
index c2e7b2a47..40d25d144 100644
--- a/packages/anastasis-webui/src/context/anastasis.ts
+++ b/packages/anastasis-webui/src/context/anastasis.ts
@@ -23,11 +23,9 @@ import { createContext, h, VNode } from "preact";
import { useContext } from "preact/hooks";
import { AnastasisReducerApi } from "../hooks/use-anastasis-reducer";
-type Type = AnastasisReducerApi | undefined;
-
const initial = undefined;
-const Context = createContext<Type>(initial);
+const Context = createContext<AnastasisReducerApi | undefined>(initial);
interface Props {
value: AnastasisReducerApi;
@@ -38,4 +36,5 @@ export const AnastasisProvider = ({ value, children }: Props): VNode => {
return h(Context.Provider, { value, children });
};
-export const useAnastasisContext = (): Type => useContext(Context);
+export const useAnastasisContext = (): AnastasisReducerApi | undefined =>
+ useContext(Context);
diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
index b18610427..321cf3f0a 100644
--- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
+++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
@@ -1,8 +1,12 @@
import { TalerErrorCode } from "@gnu-taler/taler-util";
import {
+ AggregatedPolicyMetaInfo,
BackupStates,
+ discoverPolicies,
+ DiscoveryCursor,
getBackupStartState,
getRecoveryStartState,
+ PolicyMetaInfo,
RecoveryStates,
reduceAction,
ReducerState,
@@ -15,6 +19,7 @@ const remoteReducer = false;
interface AnastasisState {
reducerState: ReducerState | undefined;
currentError: any;
+ discoveryState: DiscoveryUiState;
}
async function getBackupStartStateRemote(): Promise<ReducerState> {
@@ -98,9 +103,21 @@ export interface ReducerTransactionHandle {
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 {
currentReducerState: ReducerState | undefined;
currentError: any;
+ discoveryState: DiscoveryUiState;
dismissError: () => void;
startBackup: () => void;
startRecover: () => void;
@@ -109,6 +126,8 @@ export interface AnastasisReducerApi {
transition(action: string, args: any): Promise<void>;
exportState: () => string;
importState: (s: string) => void;
+ discoverStart(): Promise<void>;
+ discoverMore(): Promise<void>;
/**
* Run multiple reducer steps in a transaction without
* affecting the UI-visible transition state in-between.
@@ -152,6 +171,9 @@ export function useAnastasisReducer(): AnastasisReducerApi {
() => ({
reducerState: getStateFromStorage(),
currentError: undefined,
+ discoveryState: {
+ state: "none",
+ },
}),
);
@@ -192,6 +214,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
return {
currentReducerState: anastasisState.reducerState,
currentError: anastasisState.currentError,
+ discoveryState: anastasisState.discoveryState,
async startBackup() {
let s: ReducerState;
if (remoteReducer) {
@@ -213,17 +236,59 @@ export function useAnastasisReducer(): AnastasisReducerApi {
}
},
exportState() {
- const state = getStateFromStorage()
- return JSON.stringify(state)
+ const state = getStateFromStorage();
+ return JSON.stringify(state);
},
importState(s: string) {
try {
- const state = JSON.parse(s)
- setAnastasisState({ reducerState: state, currentError: undefined })
+ const state = JSON.parse(s);
+ setAnastasisState({
+ reducerState: state,
+ currentError: undefined,
+ discoveryState: {
+ state: "none",
+ },
+ });
} 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() {
let s: ReducerState;
if (remoteReducer) {
@@ -301,7 +366,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
}
class ReducerTxImpl implements ReducerTransactionHandle {
- constructor(public transactionState: ReducerState) { }
+ constructor(public transactionState: ReducerState) {}
async transition(action: string, args: any): Promise<ReducerState> {
let s: ReducerState;
if (remoteReducer) {
diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
index 076d205b6..84f0303fe 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
@@ -1,9 +1,10 @@
import {
AuthenticationProviderStatus,
AuthenticationProviderStatusOk,
+ PolicyMetaInfo,
} from "@gnu-taler/anastasis-core";
import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
import { AsyncButton } from "../../components/AsyncButton";
import { PhoneNumberInput } from "../../components/fields/NumberInput";
import { useAnastasisContext } from "../../context/anastasis";
@@ -13,8 +14,100 @@ import { AnastasisClientFrame } from "./index";
export function SecretSelectionScreen(): 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));
+ }, []);
+
+ 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 =
(reducer?.currentReducerState &&
"recovery_document" in reducer.currentReducerState &&
@@ -71,15 +164,16 @@ export function SecretSelectionScreen(): VNode {
return <AddingProviderScreen onCancel={() => setManageProvider(false)} />;
}
- const provierInfo = provs[
+ const providerInfo = provs[
recoveryDocument.provider_url
] as AuthenticationProviderStatusOk;
+
return (
<AnastasisClientFrame title="Recovery: Select secret">
<div class="columns">
<div class="column">
<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">
{currentVersion === 0 ? (
<p>Set to recover the latest version</p>