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