anastasis: make recovery work, at least for security questions
This commit is contained in:
parent
0ee669f523
commit
3740010117
@ -185,6 +185,7 @@ async function anastasisDecrypt(
|
|||||||
export const asOpaque = (x: string): OpaqueData => x;
|
export const asOpaque = (x: string): OpaqueData => x;
|
||||||
const asEncryptedKeyShare = (x: OpaqueData): EncryptedKeyShare => x as string;
|
const asEncryptedKeyShare = (x: OpaqueData): EncryptedKeyShare => x as string;
|
||||||
const asEncryptedTruth = (x: OpaqueData): EncryptedTruth => x as string;
|
const asEncryptedTruth = (x: OpaqueData): EncryptedTruth => x as string;
|
||||||
|
const asKeyShare = (x: OpaqueData): KeyShare => x as string;
|
||||||
|
|
||||||
export async function encryptKeyshare(
|
export async function encryptKeyshare(
|
||||||
keyShare: KeyShare,
|
keyShare: KeyShare,
|
||||||
@ -198,6 +199,17 @@ export async function encryptKeyshare(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function decryptKeyShare(
|
||||||
|
encKeyShare: EncryptedKeyShare,
|
||||||
|
userId: UserIdentifier,
|
||||||
|
answerSalt?: string,
|
||||||
|
): Promise<KeyShare> {
|
||||||
|
const s = answerSalt ?? "eks";
|
||||||
|
return asKeyShare(
|
||||||
|
await anastasisDecrypt(asOpaque(userId), asOpaque(encKeyShare), s),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function encryptTruth(
|
export async function encryptTruth(
|
||||||
nonce: EncryptionNonce,
|
nonce: EncryptionNonce,
|
||||||
truthEncKey: TruthKey,
|
truthEncKey: TruthKey,
|
||||||
@ -226,6 +238,20 @@ export interface CoreSecretEncResult {
|
|||||||
encMasterKeys: EncryptedMasterKey[];
|
encMasterKeys: EncryptedMasterKey[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function coreSecretRecover(args: {
|
||||||
|
encryptedMasterKey: OpaqueData;
|
||||||
|
policyKey: PolicyKey;
|
||||||
|
encryptedCoreSecret: OpaqueData;
|
||||||
|
}): Promise<OpaqueData> {
|
||||||
|
const masterKey = await anastasisDecrypt(
|
||||||
|
asOpaque(args.policyKey),
|
||||||
|
args.encryptedMasterKey,
|
||||||
|
"emk",
|
||||||
|
);
|
||||||
|
console.log("recovered master key", masterKey);
|
||||||
|
return await anastasisDecrypt(masterKey, args.encryptedCoreSecret, "cse");
|
||||||
|
}
|
||||||
|
|
||||||
export async function coreSecretEncrypt(
|
export async function coreSecretEncrypt(
|
||||||
policyKeys: PolicyKey[],
|
policyKeys: PolicyKey[],
|
||||||
coreSecret: OpaqueData,
|
coreSecret: OpaqueData,
|
||||||
|
@ -26,7 +26,8 @@ import {
|
|||||||
ActionArgEnterSecret,
|
ActionArgEnterSecret,
|
||||||
ActionArgEnterSecretName,
|
ActionArgEnterSecretName,
|
||||||
ActionArgEnterUserAttributes,
|
ActionArgEnterUserAttributes,
|
||||||
ActionArgSelectChallenge,
|
ActionArgsSelectChallenge,
|
||||||
|
ActionArgsSolveChallengeRequest,
|
||||||
AuthenticationProviderStatus,
|
AuthenticationProviderStatus,
|
||||||
AuthenticationProviderStatusOk,
|
AuthenticationProviderStatusOk,
|
||||||
AuthMethod,
|
AuthMethod,
|
||||||
@ -66,6 +67,9 @@ import {
|
|||||||
userIdentifierDerive,
|
userIdentifierDerive,
|
||||||
typedArrayConcat,
|
typedArrayConcat,
|
||||||
decryptRecoveryDocument,
|
decryptRecoveryDocument,
|
||||||
|
decryptKeyShare,
|
||||||
|
KeyShare,
|
||||||
|
coreSecretRecover,
|
||||||
} from "./crypto.js";
|
} from "./crypto.js";
|
||||||
import { unzlibSync, zlibSync } from "fflate";
|
import { unzlibSync, zlibSync } from "fflate";
|
||||||
import { EscrowMethod, RecoveryDocument } from "./recovery-document-types.js";
|
import { EscrowMethod, RecoveryDocument } from "./recovery-document-types.js";
|
||||||
@ -626,8 +630,10 @@ async function downloadPolicy(
|
|||||||
const providerUrls = Object.keys(state.authentication_providers ?? {});
|
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]: AuthenticationProviderStatus } = {};
|
const newProviderStatus: { [url: string]: AuthenticationProviderStatusOk } =
|
||||||
|
{};
|
||||||
const userAttributes = state.identity_attributes!;
|
const userAttributes = state.identity_attributes!;
|
||||||
|
// FIXME: Shouldn't we also store the status of bad providers?
|
||||||
for (const url of providerUrls) {
|
for (const url of providerUrls) {
|
||||||
const pi = await getProviderInfo(url);
|
const pi = await getProviderInfo(url);
|
||||||
if ("error_code" in pi || !("http_status" in pi)) {
|
if ("error_code" in pi || !("http_status" in pi)) {
|
||||||
@ -635,6 +641,12 @@ async function downloadPolicy(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
newProviderStatus[url] = pi;
|
newProviderStatus[url] = pi;
|
||||||
|
}
|
||||||
|
for (const url of providerUrls) {
|
||||||
|
const pi = newProviderStatus[url];
|
||||||
|
if (!pi) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const userId = await userIdentifierDerive(userAttributes, pi.salt);
|
const userId = await userIdentifierDerive(userAttributes, pi.salt);
|
||||||
const acctKeypair = accountKeypairDerive(userId);
|
const acctKeypair = accountKeypairDerive(userId);
|
||||||
const resp = await fetch(new URL(`policy/${acctKeypair.pub}`, url).href);
|
const resp = await fetch(new URL(`policy/${acctKeypair.pub}`, url).href);
|
||||||
@ -670,7 +682,7 @@ async function downloadPolicy(
|
|||||||
}
|
}
|
||||||
const recoveryInfo: RecoveryInformation = {
|
const recoveryInfo: RecoveryInformation = {
|
||||||
challenges: recoveryDoc.escrow_methods.map((x) => {
|
challenges: recoveryDoc.escrow_methods.map((x) => {
|
||||||
console.log("providers", state.authentication_providers);
|
console.log("providers", newProviderStatus);
|
||||||
const prov = newProviderStatus[x.url] as AuthenticationProviderStatusOk;
|
const prov = newProviderStatus[x.url] as AuthenticationProviderStatusOk;
|
||||||
return {
|
return {
|
||||||
cost: prov.methods.find((m) => m.type === x.escrow_type)?.usage_fee!,
|
cost: prov.methods.find((m) => m.type === x.escrow_type)?.usage_fee!,
|
||||||
@ -692,9 +704,124 @@ async function downloadPolicy(
|
|||||||
recovery_state: RecoveryStates.SecretSelecting,
|
recovery_state: RecoveryStates.SecretSelecting,
|
||||||
recovery_document: foundRecoveryInfo,
|
recovery_document: foundRecoveryInfo,
|
||||||
recovery_information: recoveryInfo,
|
recovery_information: recoveryInfo,
|
||||||
|
verbatim_recovery_document: recoveryDoc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to reconstruct the secret from the available shares.
|
||||||
|
*
|
||||||
|
* Returns the state unmodified if not enough key shares are available yet.
|
||||||
|
*/
|
||||||
|
async function tryRecoverSecret(
|
||||||
|
state: ReducerStateRecovery,
|
||||||
|
): Promise<ReducerStateRecovery | ReducerStateError> {
|
||||||
|
const rd = state.verbatim_recovery_document!;
|
||||||
|
for (const p of rd.policies) {
|
||||||
|
const keyShares: KeyShare[] = [];
|
||||||
|
let missing = false;
|
||||||
|
for (const truthUuid of p.uuids) {
|
||||||
|
const ks = (state.recovered_key_shares ?? {})[truthUuid];
|
||||||
|
if (!ks) {
|
||||||
|
missing = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
keyShares.push(ks);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const policyKey = await policyKeyDerive(keyShares, p.salt);
|
||||||
|
const coreSecretBytes = await coreSecretRecover({
|
||||||
|
encryptedCoreSecret: rd.encrypted_core_secret,
|
||||||
|
encryptedMasterKey: p.master_key,
|
||||||
|
policyKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
recovery_state: RecoveryStates.RecoveryFinished,
|
||||||
|
selected_challenge_uuid: undefined,
|
||||||
|
core_secret: JSON.parse(bytesToString(decodeCrock(coreSecretBytes))),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ...state };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function solveChallenge(
|
||||||
|
state: ReducerStateRecovery,
|
||||||
|
ta: ActionArgsSolveChallengeRequest,
|
||||||
|
): Promise<ReducerStateRecovery | ReducerStateError> {
|
||||||
|
const recDoc: RecoveryDocument = state.verbatim_recovery_document!;
|
||||||
|
const truth = recDoc.escrow_methods.find(
|
||||||
|
(x) => x.uuid === state.selected_challenge_uuid,
|
||||||
|
);
|
||||||
|
if (!truth) {
|
||||||
|
throw "truth for challenge not found";
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(`/truth/${truth.uuid}`, truth.url);
|
||||||
|
|
||||||
|
// FIXME: This isn't correct for non-question truth responses.
|
||||||
|
url.searchParams.set(
|
||||||
|
"response",
|
||||||
|
await secureAnswerHash(ta.answer, truth.uuid, truth.truth_salt),
|
||||||
|
);
|
||||||
|
|
||||||
|
const resp = await fetch(url.href, {
|
||||||
|
headers: {
|
||||||
|
"Anastasis-Truth-Decryption-Key": truth.truth_key,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(resp);
|
||||||
|
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
return {
|
||||||
|
code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED,
|
||||||
|
hint: "got non-200 response",
|
||||||
|
http_status: resp.status,
|
||||||
|
} as ReducerStateError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const answerSalt = truth.escrow_type === "question" ? ta.answer : undefined;
|
||||||
|
|
||||||
|
const userId = await userIdentifierDerive(
|
||||||
|
state.identity_attributes,
|
||||||
|
truth.provider_salt,
|
||||||
|
);
|
||||||
|
|
||||||
|
const respBody = new Uint8Array(await resp.arrayBuffer());
|
||||||
|
const keyShare = await decryptKeyShare(
|
||||||
|
encodeCrock(respBody),
|
||||||
|
userId,
|
||||||
|
answerSalt,
|
||||||
|
);
|
||||||
|
|
||||||
|
const recoveredKeyShares = {
|
||||||
|
...(state.recovered_key_shares ?? {}),
|
||||||
|
[truth.uuid]: keyShare,
|
||||||
|
};
|
||||||
|
|
||||||
|
const challengeFeedback = {
|
||||||
|
...state.challenge_feedback,
|
||||||
|
[truth.uuid]: {
|
||||||
|
state: "solved",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const newState: ReducerStateRecovery = {
|
||||||
|
...state,
|
||||||
|
recovery_state: RecoveryStates.ChallengeSelecting,
|
||||||
|
challenge_feedback: challengeFeedback,
|
||||||
|
recovered_key_shares: recoveredKeyShares,
|
||||||
|
};
|
||||||
|
|
||||||
|
return tryRecoverSecret(newState);
|
||||||
|
}
|
||||||
|
|
||||||
async function recoveryEnterUserAttributes(
|
async function recoveryEnterUserAttributes(
|
||||||
state: ReducerStateRecovery,
|
state: ReducerStateRecovery,
|
||||||
attributes: Record<string, string>,
|
attributes: Record<string, string>,
|
||||||
@ -707,6 +834,33 @@ async function recoveryEnterUserAttributes(
|
|||||||
return downloadPolicy(st);
|
return downloadPolicy(st);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function selectChallenge(
|
||||||
|
state: ReducerStateRecovery,
|
||||||
|
ta: ActionArgsSelectChallenge,
|
||||||
|
): Promise<ReducerStateRecovery | ReducerStateError> {
|
||||||
|
const recDoc: RecoveryDocument = state.verbatim_recovery_document!;
|
||||||
|
const truth = recDoc.escrow_methods.find((x) => x.uuid === ta.uuid);
|
||||||
|
if (!truth) {
|
||||||
|
throw "truth for challenge not found";
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(`/truth/${truth.uuid}`, truth.url);
|
||||||
|
|
||||||
|
const resp = await fetch(url.href, {
|
||||||
|
headers: {
|
||||||
|
"Anastasis-Truth-Decryption-Key": truth.truth_key,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(resp);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
recovery_state: RecoveryStates.ChallengeSolving,
|
||||||
|
selected_challenge_uuid: ta.uuid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function reduceAction(
|
export async function reduceAction(
|
||||||
state: ReducerState,
|
state: ReducerState,
|
||||||
action: string,
|
action: string,
|
||||||
@ -989,17 +1143,22 @@ export async function reduceAction(
|
|||||||
|
|
||||||
if (state.recovery_state === RecoveryStates.ChallengeSelecting) {
|
if (state.recovery_state === RecoveryStates.ChallengeSelecting) {
|
||||||
if (action === "select_challenge") {
|
if (action === "select_challenge") {
|
||||||
const ta: ActionArgSelectChallenge = args;
|
const ta: ActionArgsSelectChallenge = args;
|
||||||
return {
|
return selectChallenge(state, ta);
|
||||||
...state,
|
|
||||||
recovery_state: RecoveryStates.ChallengeSolving,
|
|
||||||
selected_challenge_uuid: ta.uuid,
|
|
||||||
};
|
|
||||||
} else if (action === "back") {
|
} else if (action === "back") {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
recovery_state: RecoveryStates.SecretSelecting,
|
recovery_state: RecoveryStates.SecretSelecting,
|
||||||
};
|
};
|
||||||
|
} else if (action === "next") {
|
||||||
|
const s2 = await tryRecoverSecret(state);
|
||||||
|
if (s2.recovery_state === RecoveryStates.RecoveryFinished) {
|
||||||
|
return s2;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
|
||||||
|
hint: "Not enough challenges solved",
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
|
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
|
||||||
@ -1010,12 +1169,34 @@ export async function reduceAction(
|
|||||||
|
|
||||||
if (state.recovery_state === RecoveryStates.ChallengeSolving) {
|
if (state.recovery_state === RecoveryStates.ChallengeSolving) {
|
||||||
if (action === "back") {
|
if (action === "back") {
|
||||||
const ta: ActionArgSelectChallenge = args;
|
const ta: ActionArgsSelectChallenge = args;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
selected_challenge_uuid: undefined,
|
selected_challenge_uuid: undefined,
|
||||||
recovery_state: RecoveryStates.ChallengeSelecting,
|
recovery_state: RecoveryStates.ChallengeSelecting,
|
||||||
};
|
};
|
||||||
|
} else if (action === "solve_challenge") {
|
||||||
|
const ta: ActionArgsSolveChallengeRequest = args;
|
||||||
|
return solveChallenge(state, ta);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
|
||||||
|
hint: `Unsupported action '${action}'`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.recovery_state === RecoveryStates.RecoveryFinished) {
|
||||||
|
if (action === "back") {
|
||||||
|
const ta: ActionArgsSelectChallenge = args;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selected_challenge_uuid: undefined,
|
||||||
|
recovery_state: RecoveryStates.ChallengeSelecting,
|
||||||
|
};
|
||||||
|
} else if (action === "solve_challenge") {
|
||||||
|
const ta: ActionArgsSolveChallengeRequest = args;
|
||||||
|
return solveChallenge(state, ta);
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
|
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
|
||||||
|
@ -1,22 +1,37 @@
|
|||||||
import { TruthKey, TruthSalt, TruthUuid } from "./crypto.js";
|
import { TruthKey, TruthSalt, TruthUuid } from "./crypto.js";
|
||||||
|
|
||||||
export interface RecoveryDocument {
|
export interface RecoveryDocument {
|
||||||
// Human-readable name of the secret
|
/**
|
||||||
|
* Human-readable name of the secret
|
||||||
|
* FIXME: Why is this optional?
|
||||||
|
*/
|
||||||
secret_name?: string;
|
secret_name?: string;
|
||||||
|
|
||||||
// Encrypted core secret.
|
/**
|
||||||
encrypted_core_secret: string; // bytearray of undefined length
|
* Encrypted core secret.
|
||||||
|
*
|
||||||
|
* Variable-size length, base32-crock encoded.
|
||||||
|
*/
|
||||||
|
encrypted_core_secret: string;
|
||||||
|
|
||||||
// List of escrow providers and selected authentication method.
|
/**
|
||||||
|
* List of escrow providers and selected authentication method.
|
||||||
|
*/
|
||||||
escrow_methods: EscrowMethod[];
|
escrow_methods: EscrowMethod[];
|
||||||
|
|
||||||
// List of possible decryption policies.
|
/**
|
||||||
|
* List of possible decryption policies.
|
||||||
|
*/
|
||||||
policies: DecryptionPolicy[];
|
policies: DecryptionPolicy[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DecryptionPolicy {
|
export interface DecryptionPolicy {
|
||||||
// Salt included to encrypt master key share when
|
/**
|
||||||
// using this decryption policy.
|
* Salt included to encrypt master key share when
|
||||||
|
* using this decryption policy.
|
||||||
|
*
|
||||||
|
* FIXME: Rename to policy_salt
|
||||||
|
*/
|
||||||
salt: string;
|
salt: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,12 +58,16 @@ export interface EscrowMethod {
|
|||||||
*/
|
*/
|
||||||
escrow_type: string;
|
escrow_type: string;
|
||||||
|
|
||||||
// UUID of the escrow method.
|
/**
|
||||||
// 16 bytes base32-crock encoded.
|
* UUID of the escrow method.
|
||||||
|
* 16 bytes base32-crock encoded.
|
||||||
|
*/
|
||||||
uuid: TruthUuid;
|
uuid: TruthUuid;
|
||||||
|
|
||||||
// Key used to encrypt the Truth this EscrowMethod is related to.
|
/**
|
||||||
// Client has to provide this key to the server when using /truth/.
|
* 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: TruthKey;
|
truth_key: TruthKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,7 +79,9 @@ export interface EscrowMethod {
|
|||||||
// at this provider.
|
// at this provider.
|
||||||
provider_salt: string;
|
provider_salt: string;
|
||||||
|
|
||||||
// The instructions to give to the user (i.e. the security question
|
/**
|
||||||
// if this is challenge-response).
|
* The instructions to give to the user (i.e. the security question
|
||||||
|
* if this is challenge-response).
|
||||||
|
*/
|
||||||
instructions: string;
|
instructions: string;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { Duration, Timestamp } from "@gnu-taler/taler-util";
|
import { Duration, Timestamp } from "@gnu-taler/taler-util";
|
||||||
|
import { KeyShare } from "./crypto.js";
|
||||||
|
import { RecoveryDocument } from "./recovery-document-types.js";
|
||||||
|
|
||||||
export type ReducerState =
|
export type ReducerState =
|
||||||
| ReducerStateBackup
|
| ReducerStateBackup
|
||||||
@ -110,8 +112,16 @@ export interface RecoveryInformation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ReducerStateRecovery {
|
export interface ReducerStateRecovery {
|
||||||
backup_state?: undefined;
|
|
||||||
recovery_state: RecoveryStates;
|
recovery_state: RecoveryStates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unused in the recovery states.
|
||||||
|
*/
|
||||||
|
backup_state?: undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unused in the recovery states.
|
||||||
|
*/
|
||||||
code?: undefined;
|
code?: undefined;
|
||||||
|
|
||||||
identity_attributes?: { [n: string]: string };
|
identity_attributes?: { [n: string]: string };
|
||||||
@ -133,10 +143,18 @@ export interface ReducerStateRecovery {
|
|||||||
// FIXME: This should really be renamed to recovery_internal_data
|
// FIXME: This should really be renamed to recovery_internal_data
|
||||||
recovery_document?: RecoveryInternalData;
|
recovery_document?: RecoveryInternalData;
|
||||||
|
|
||||||
|
// FIXME: The C reducer should also use this!
|
||||||
|
verbatim_recovery_document?: RecoveryDocument;
|
||||||
|
|
||||||
selected_challenge_uuid?: string;
|
selected_challenge_uuid?: string;
|
||||||
|
|
||||||
challenge_feedback?: { [uuid: string]: ChallengeFeedback };
|
challenge_feedback?: { [uuid: string]: ChallengeFeedback };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key shares that we managed to recover so far.
|
||||||
|
*/
|
||||||
|
recovered_key_shares?: { [truth_uuid: string]: KeyShare };
|
||||||
|
|
||||||
core_secret?: {
|
core_secret?: {
|
||||||
mime: string;
|
mime: string;
|
||||||
value: string;
|
value: string;
|
||||||
@ -254,6 +272,12 @@ export interface ActionArgEnterSecret {
|
|||||||
expiration: Duration;
|
expiration: Duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionArgSelectChallenge {
|
export interface ActionArgsSelectChallenge {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ActionArgsSolveChallengeRequest = SolveChallengeAnswerRequest;
|
||||||
|
|
||||||
|
export interface SolveChallengeAnswerRequest {
|
||||||
|
answer: string;
|
||||||
|
}
|
||||||
|
@ -8,7 +8,6 @@ import { RecoveryReducerProps, AnastasisClientFrame } from "./index";
|
|||||||
export function RecoveryFinishedScreen(props: RecoveryReducerProps): VNode {
|
export function RecoveryFinishedScreen(props: RecoveryReducerProps): VNode {
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame title="Recovery Finished" hideNext>
|
<AnastasisClientFrame title="Recovery Finished" hideNext>
|
||||||
<h1>Recovery Finished</h1>
|
|
||||||
<p>
|
<p>
|
||||||
Secret: {bytesToString(decodeCrock(props.recoveryState.core_secret?.value!))}
|
Secret: {bytesToString(decodeCrock(props.recoveryState.core_secret?.value!))}
|
||||||
</p>
|
</p>
|
||||||
|
Loading…
Reference in New Issue
Block a user