Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | 3x 3x 41x 41x 1092x 41x 54x 54x 1795x 54x 4x 4x 4x 4x 4x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 9x 9x 9x 9x 9x 9x 9x 9x 3x 6x | import { gcm } from '@noble/ciphers/aes';
import { pbkdf2 } from '@noble/hashes/pbkdf2';
import { sha256 } from '@noble/hashes/sha2';
import { keccak_256 as keccak256 } from '@noble/hashes/sha3';
const harden = globalThis.harden ?? (<T>(value: T): T => value);
const DEFAULT_PBKDF2_ITERATIONS = 600_000;
/**
* Encrypted mnemonic envelope persisted in baggage.
*/
export type EncryptedMnemonicData = {
encrypted: true;
ciphertext: string;
nonce: string;
salt: string;
};
/**
* Convert a hex-encoded string (no 0x prefix) to Uint8Array.
*
* @param encoded - Hex-encoded string without 0x prefix.
* @returns The byte array.
*/
function hexToBytes(encoded: string): Uint8Array {
const bytes = new Uint8Array(encoded.length / 2);
for (let i = 0; i < encoded.length; i += 2) {
bytes[i / 2] = parseInt(encoded.slice(i, i + 2), 16);
}
return bytes;
}
/**
* Convert a Uint8Array to a hex-encoded string (no 0x prefix).
*
* @param bytes - The byte array.
* @returns Hex-encoded string without 0x prefix.
*/
function bytesToHex(bytes: Uint8Array): string {
let result = '';
for (const byte of bytes) {
result += byte.toString(16).padStart(2, '0');
}
return result;
}
/**
* Derive a 16-byte salt from the password using keccak256 with a domain separator.
* Used as fallback when no external salt is provided.
*
* @param passwordBytes - The UTF-8 encoded password.
* @returns A 16-byte salt.
*/
function deriveSalt(passwordBytes: Uint8Array): Uint8Array {
const domainSeparator = new TextEncoder().encode('ocap-keyring-salt');
const input = new Uint8Array(domainSeparator.length + passwordBytes.length);
input.set(domainSeparator);
input.set(passwordBytes, domainSeparator.length);
return keccak256(input).slice(0, 16);
}
/**
* Derive a 12-byte nonce from salt and derived key using keccak256.
*
* @param salt - The salt bytes.
* @param key - The derived AES key bytes.
* @returns A 12-byte nonce for AES-GCM.
*/
function deriveNonce(salt: Uint8Array, key: Uint8Array): Uint8Array {
const input = new Uint8Array(salt.length + key.length);
input.set(salt);
input.set(key, salt.length);
return keccak256(input).slice(0, 12);
}
/**
* Encrypt a mnemonic with a password using AES-256-GCM + PBKDF2.
*
* @param options - Encryption options.
* @param options.mnemonic - The mnemonic to encrypt.
* @param options.password - The password for key derivation.
* @param options.salt - Optional hex-encoded salt (16 bytes). If omitted, derived from password.
* @param options.pbkdf2Iterations - Optional PBKDF2 iteration count. Defaults to 600,000.
* @returns The encrypted mnemonic envelope.
*/
export function encryptMnemonic({
mnemonic,
password,
salt: saltEncoded,
pbkdf2Iterations = DEFAULT_PBKDF2_ITERATIONS,
}: {
mnemonic: string;
password: string;
salt?: string;
pbkdf2Iterations?: number;
}): EncryptedMnemonicData {
const passwordBytes = new TextEncoder().encode(password);
const salt = saltEncoded
? hexToBytes(saltEncoded)
: deriveSalt(passwordBytes);
const key = pbkdf2(sha256, passwordBytes, salt, {
c: pbkdf2Iterations,
dkLen: 32,
});
const nonce = deriveNonce(salt, key);
const cipher = gcm(key, nonce);
const plaintext = new TextEncoder().encode(mnemonic);
const ciphertext = cipher.encrypt(plaintext);
return harden({
encrypted: true as const,
ciphertext: bytesToHex(ciphertext),
nonce: bytesToHex(nonce),
salt: bytesToHex(salt),
});
}
/**
* Decrypt a mnemonic from an encrypted envelope.
*
* @param options - Decryption options.
* @param options.data - The encrypted mnemonic envelope.
* @param options.password - The password used during encryption.
* @param options.pbkdf2Iterations - Optional PBKDF2 iteration count. Must match the value used during encryption.
* @returns The decrypted mnemonic string.
*/
export function decryptMnemonic({
data,
password,
pbkdf2Iterations = DEFAULT_PBKDF2_ITERATIONS,
}: {
data: EncryptedMnemonicData;
password: string;
pbkdf2Iterations?: number;
}): string {
const passwordBytes = new TextEncoder().encode(password);
const salt = hexToBytes(data.salt);
const nonce = hexToBytes(data.nonce);
const ciphertext = hexToBytes(data.ciphertext);
const key = pbkdf2(sha256, passwordBytes, salt, {
c: pbkdf2Iterations,
dkLen: 32,
});
const cipher = gcm(key, nonce);
let plaintext: Uint8Array;
try {
plaintext = cipher.decrypt(ciphertext);
} catch {
throw new Error('Decryption failed — check that the password is correct');
}
return new TextDecoder().decode(plaintext);
}
|