anastasis-core: compatible secret upload
This commit is contained in:
parent
5dc0089392
commit
6c5d32be74
@ -23,6 +23,7 @@
|
||||
"dependencies": {
|
||||
"@gnu-taler/taler-util": "workspace:^0.8.3",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"fflate": "^0.6.0",
|
||||
"hash-wasm": "^4.9.0",
|
||||
"node-fetch": "^3.0.0"
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
import test from "ava";
|
||||
import {
|
||||
accountKeypairDerive,
|
||||
encryptKeyshare,
|
||||
encryptTruth,
|
||||
policyKeyDerive,
|
||||
secureAnswerHash,
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
crypto_sign_keyPair_fromSeed,
|
||||
stringToBytes,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { gzipSync } from "fflate";
|
||||
import { argon2id } from "hash-wasm";
|
||||
|
||||
export type Flavor<T, FlavorT extends string> = T & {
|
||||
@ -84,21 +85,25 @@ export function accountKeypairDerive(userId: UserIdentifier): AccountKeyPair {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the recovery document.
|
||||
*
|
||||
* The caller should first compress the recovery doc.
|
||||
*/
|
||||
export async function encryptRecoveryDocument(
|
||||
userId: UserIdentifier,
|
||||
recoveryDoc: any,
|
||||
recoveryDocData: OpaqueData,
|
||||
): Promise<OpaqueData> {
|
||||
const plaintext = stringToBytes(JSON.stringify(recoveryDoc));
|
||||
const nonce = encodeCrock(getRandomBytes(nonceSize));
|
||||
return anastasisEncrypt(
|
||||
nonce,
|
||||
asOpaque(userId),
|
||||
encodeCrock(plaintext),
|
||||
recoveryDocData,
|
||||
"erd",
|
||||
);
|
||||
}
|
||||
|
||||
function taConcat(chunks: Uint8Array[]): Uint8Array {
|
||||
export function typedArrayConcat(chunks: Uint8Array[]): Uint8Array {
|
||||
let payloadLen = 0;
|
||||
for (const c of chunks) {
|
||||
payloadLen += c.byteLength;
|
||||
@ -120,7 +125,7 @@ export async function policyKeyDerive(
|
||||
const chunks = keyShares.map((x) => decodeCrock(x));
|
||||
const polKey = kdfKw({
|
||||
outputLength: 64,
|
||||
ikm: taConcat(chunks),
|
||||
ikm: typedArrayConcat(chunks),
|
||||
salt: decodeCrock(policySalt),
|
||||
info: stringToBytes("anastasis-policy-key-derive"),
|
||||
});
|
||||
@ -150,7 +155,7 @@ async function anastasisEncrypt(
|
||||
const key = await deriveKey(keySeed, nonce, salt);
|
||||
const nonceBuf = decodeCrock(nonce);
|
||||
const cipherText = secretbox(decodeCrock(plaintext), decodeCrock(nonce), key);
|
||||
return encodeCrock(taConcat([nonceBuf, cipherText]));
|
||||
return encodeCrock(typedArrayConcat([nonceBuf, cipherText]));
|
||||
}
|
||||
|
||||
export const asOpaque = (x: string): OpaqueData => x;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
AmountString,
|
||||
buildSigPS,
|
||||
bytesToString,
|
||||
decodeCrock,
|
||||
eddsaSign,
|
||||
encodeCrock,
|
||||
@ -58,7 +59,9 @@ import {
|
||||
TruthUuid,
|
||||
UserIdentifier,
|
||||
userIdentifierDerive,
|
||||
typedArrayConcat,
|
||||
} from "./crypto.js";
|
||||
import { zlibSync } from "fflate";
|
||||
|
||||
const { fetch, Request, Response, Headers } = fetchPonyfill({});
|
||||
|
||||
@ -93,7 +96,7 @@ interface DecryptionPolicy {
|
||||
/**
|
||||
* List of escrow methods identified by their UUID.
|
||||
*/
|
||||
uuid: string[];
|
||||
uuids: string[];
|
||||
}
|
||||
|
||||
interface EscrowMethod {
|
||||
@ -115,8 +118,10 @@ interface EscrowMethod {
|
||||
// Client has to provide this key to the server when using /truth/.
|
||||
truth_key: TruthKey;
|
||||
|
||||
// Salt used to encrypt the truth on the Anastasis server.
|
||||
salt: string;
|
||||
/**
|
||||
* Salt to hash the security question answer if applicable.
|
||||
*/
|
||||
truth_salt: TruthSalt;
|
||||
|
||||
// Salt from the provider to derive the user ID
|
||||
// at this provider.
|
||||
@ -401,7 +406,11 @@ async function getTruthValue(
|
||||
switch (authMethod.type) {
|
||||
case "question": {
|
||||
return asOpaque(
|
||||
await secureAnswerHash(authMethod.challenge, truthUuid, questionSalt),
|
||||
await secureAnswerHash(
|
||||
bytesToString(decodeCrock(authMethod.challenge)),
|
||||
truthUuid,
|
||||
questionSalt,
|
||||
),
|
||||
);
|
||||
}
|
||||
case "sms":
|
||||
@ -414,12 +423,28 @@ async function getTruthValue(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress the recovery document and add a size header.
|
||||
*/
|
||||
async function compressRecoveryDoc(rd: any): Promise<Uint8Array> {
|
||||
console.log("recovery document", rd);
|
||||
const docBytes = stringToBytes(JSON.stringify(rd));
|
||||
console.log("plain doc length", docBytes.length);
|
||||
const sizeHeaderBuf = new ArrayBuffer(4);
|
||||
const dvbuf = new DataView(sizeHeaderBuf);
|
||||
dvbuf.setUint32(0, docBytes.length, false);
|
||||
const zippedDoc = zlibSync(docBytes);
|
||||
return typedArrayConcat([new Uint8Array(sizeHeaderBuf), zippedDoc]);
|
||||
}
|
||||
|
||||
async function uploadSecret(
|
||||
state: ReducerStateBackup,
|
||||
): Promise<ReducerStateBackup | ReducerStateError> {
|
||||
const policies = state.policies!;
|
||||
const secretName = state.secret_name!;
|
||||
const coreSecret = state.core_secret?.value!;
|
||||
const coreSecret: OpaqueData = encodeCrock(
|
||||
stringToBytes(JSON.stringify(state.core_secret!)),
|
||||
);
|
||||
// Truth key is `${methodIndex}/${providerUrl}`
|
||||
const truthMetadataMap: Record<string, TruthMetaData> = {};
|
||||
|
||||
@ -435,8 +460,8 @@ async function uploadSecret(
|
||||
const methUuids: 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]) {
|
||||
const truthReference = `${meth.authentication_method}:${meth.provider}`;
|
||||
if (truthMetadataMap[truthReference]) {
|
||||
continue;
|
||||
}
|
||||
const keyShare = encodeCrock(getRandomBytes(32));
|
||||
@ -445,15 +470,16 @@ async function uploadSecret(
|
||||
key_share: keyShare,
|
||||
nonce: encodeCrock(getRandomBytes(24)),
|
||||
truth_salt: encodeCrock(getRandomBytes(16)),
|
||||
truth_key: encodeCrock(getRandomBytes(32)),
|
||||
truth_key: encodeCrock(getRandomBytes(64)),
|
||||
uuid: encodeCrock(getRandomBytes(32)),
|
||||
pol_method_index: methIndex,
|
||||
policy_index: policyIndex,
|
||||
};
|
||||
methUuids.push(tm.uuid);
|
||||
truthMetadataMap[truthKey] = tm;
|
||||
truthMetadataMap[truthReference] = tm;
|
||||
}
|
||||
const policyKey = await policyKeyDerive(keyShares, policySalt);
|
||||
policyUuids.push(methUuids);
|
||||
policyKeys.push(policyKey);
|
||||
policySalts.push(policySalt);
|
||||
}
|
||||
@ -492,7 +518,9 @@ async function uploadSecret(
|
||||
const encryptedKeyShare = await encryptKeyshare(
|
||||
tm.key_share,
|
||||
uid,
|
||||
tm.truth_salt,
|
||||
authMethod.type === "question"
|
||||
? bytesToString(decodeCrock(authMethod.challenge))
|
||||
: undefined,
|
||||
);
|
||||
console.log(
|
||||
"encrypted key share len",
|
||||
@ -524,7 +552,7 @@ async function uploadSecret(
|
||||
escrow_type: authMethod.type,
|
||||
instructions: authMethod.instructions,
|
||||
provider_salt: provider.salt,
|
||||
salt: tm.truth_salt,
|
||||
truth_salt: tm.truth_salt,
|
||||
truth_key: tm.truth_key,
|
||||
url: meth.provider,
|
||||
uuid: tm.uuid,
|
||||
@ -542,7 +570,7 @@ async function uploadSecret(
|
||||
policies: policies.map((x, i) => {
|
||||
return {
|
||||
master_key: csr.encMasterKeys[i],
|
||||
uuid: policyUuids[i],
|
||||
uuids: policyUuids[i],
|
||||
salt: policySalts[i],
|
||||
};
|
||||
}),
|
||||
@ -553,7 +581,12 @@ async function uploadSecret(
|
||||
for (const prov of state.policy_providers!) {
|
||||
const uid = uidMap[prov.provider_url];
|
||||
const acctKeypair = accountKeypairDerive(uid);
|
||||
const encRecoveryDoc = await encryptRecoveryDocument(uid, rd);
|
||||
const zippedDoc = await compressRecoveryDoc(rd);
|
||||
console.log("zipped doc", zippedDoc);
|
||||
const encRecoveryDoc = await encryptRecoveryDocument(
|
||||
uid,
|
||||
encodeCrock(zippedDoc),
|
||||
);
|
||||
const bodyHash = hash(decodeCrock(encRecoveryDoc));
|
||||
const sigPS = buildSigPS(TalerSignaturePurpose.ANASTASIS_POLICY_UPLOAD)
|
||||
.put(bodyHash)
|
||||
|
@ -34,6 +34,11 @@ export interface SuccessDetails {
|
||||
};
|
||||
}
|
||||
|
||||
export interface CoreSecret {
|
||||
mime: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ReducerStateBackup {
|
||||
recovery_state?: undefined;
|
||||
backup_state: BackupStates;
|
||||
@ -61,10 +66,7 @@ export interface ReducerStateBackup {
|
||||
provider: string;
|
||||
}[];
|
||||
|
||||
core_secret?: {
|
||||
mime: string;
|
||||
value: string;
|
||||
};
|
||||
core_secret?: CoreSecret;
|
||||
|
||||
expiration?: Duration;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { BackupStates, getBackupStartState, getRecoveryStartState, RecoveryState
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
const reducerBaseUrl = "http://localhost:5000/";
|
||||
const remoteReducer = true;
|
||||
const remoteReducer = false;
|
||||
|
||||
interface AnastasisState {
|
||||
reducerState: ReducerState | undefined;
|
||||
|
@ -1,20 +1,19 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import {
|
||||
encodeCrock,
|
||||
stringToBytes
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { BackupReducerProps, AnastasisClientFrame, LabeledInput } from "./index";
|
||||
import {
|
||||
BackupReducerProps,
|
||||
AnastasisClientFrame,
|
||||
LabeledInput,
|
||||
} from "./index";
|
||||
|
||||
export function SecretEditorScreen(props: BackupReducerProps): VNode {
|
||||
const { reducer } = props;
|
||||
const [secretName, setSecretName] = useState(
|
||||
props.backupState.secret_name ?? ""
|
||||
);
|
||||
const [secretValue, setSecretValue] = useState(
|
||||
props.backupState.core_secret?.value ?? "" ?? ""
|
||||
props.backupState.secret_name ?? "",
|
||||
);
|
||||
const [secretValue, setSecretValue] = useState("");
|
||||
const secretNext = (): void => {
|
||||
reducer.runTransaction(async (tx) => {
|
||||
await tx.transition("enter_secret_name", {
|
||||
@ -41,12 +40,14 @@ export function SecretEditorScreen(props: BackupReducerProps): VNode {
|
||||
<LabeledInput
|
||||
label="Secret Name:"
|
||||
grabFocus
|
||||
bind={[secretName, setSecretName]} />
|
||||
bind={[secretName, setSecretName]}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledInput
|
||||
label="Secret Value:"
|
||||
bind={[secretValue, setSecretValue]} />
|
||||
bind={[secretValue, setSecretValue]}
|
||||
/>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
|
@ -17,12 +17,14 @@ importers:
|
||||
'@gnu-taler/taler-util': workspace:^0.8.3
|
||||
ava: ^3.15.0
|
||||
fetch-ponyfill: ^7.1.0
|
||||
fflate: ^0.6.0
|
||||
hash-wasm: ^4.9.0
|
||||
node-fetch: ^3.0.0
|
||||
typescript: ^4.4.3
|
||||
dependencies:
|
||||
'@gnu-taler/taler-util': link:../taler-util
|
||||
fetch-ponyfill: 7.1.0
|
||||
fflate: 0.6.0
|
||||
hash-wasm: 4.9.0
|
||||
node-fetch: 3.0.0
|
||||
devDependencies:
|
||||
|
Loading…
Reference in New Issue
Block a user