-
Notifications
You must be signed in to change notification settings - Fork 302
Expand file tree
/
Copy pathencrypt.ts
More file actions
88 lines (81 loc) · 2.84 KB
/
encrypt.ts
File metadata and controls
88 lines (81 loc) · 2.84 KB
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
import * as sjcl from '@bitgo/sjcl';
import { randomBytes } from 'crypto';
/**
* Number of PBKDF2-SHA256 iterations used when encrypting sensitive material
* (wallet private keys, TSS key shares, GPG signing keys, etc.).
*
* History:
* 10,000 – original value set ~2014, when this took ~100 ms on contemporary
* hardware and matched OWASP guidance of the time.
* 500,000 – updated 2026 to match OWASP's current recommendation and restore
* the ~100 ms target on modern hardware (Apple Silicon, ~650 ms/op
* measured; Intel-class servers closer to 100–300 ms).
*
* Backward compatibility: the SJCL JSON envelope is self-describing – the `iter`
* field is stored in the ciphertext blob alongside `ks`, `iv`, `salt`, and `ct`.
* Decryption always reads `iter` from the blob, so existing ciphertexts encrypted
* at 10,000 iterations continue to decrypt correctly without any migration.
* Only newly encrypted blobs will use the higher iteration count.
*
* Performance (measured on Apple Silicon VM, AES-256-CCM, 238-byte plaintext):
* 10,000 iter → ~10 ms/op encrypt, ~8 ms/op decrypt (92 brute-force guesses/sec/core)
* 500,000 iter → ~540 ms/op encrypt, ~400 ms/op decrypt (~2 guesses/sec/core)
*
* The extra ~500 ms per unlock is an acceptable UX cost for a custody platform
* where key decryption happens infrequently and security is paramount.
*/
export const ENCRYPTION_ITERATIONS = 500_000;
/**
* convert a 4 element Uint8Array to a 4 byte Number
*
* @param bytes
* @return 4 byte number
*/
export function bytesToWord(bytes?: Uint8Array | number[]): number {
if (!(bytes instanceof Uint8Array) || bytes.length !== 4) {
throw new Error('bytes must be a Uint8Array with length 4');
}
return bytes.reduce((num, byte) => num * 0x100 + byte, 0);
}
export function encrypt(
password: string,
plaintext: string,
options?: {
salt?: Buffer;
iv?: Buffer;
adata?: string;
}
): string {
const salt = options?.salt || randomBytes(8);
if (salt.length !== 8) {
throw new Error(`salt must be 8 bytes`);
}
const iv = options?.iv || randomBytes(16);
if (iv.length !== 16) {
throw new Error(`iv must be 16 bytes`);
}
const encryptOptions: {
iter: number;
ks: number;
salt: number[];
iv: number[];
adata?: string;
} = {
iter: ENCRYPTION_ITERATIONS,
ks: 256,
salt: [bytesToWord(salt.slice(0, 4)), bytesToWord(salt.slice(4))],
iv: [
bytesToWord(iv.slice(0, 4)),
bytesToWord(iv.slice(4, 8)),
bytesToWord(iv.slice(8, 12)),
bytesToWord(iv.slice(12, 16)),
],
};
if (options?.adata) {
encryptOptions.adata = options.adata;
}
return sjcl.encrypt(password, plaintext, encryptOptions);
}
export function decrypt(password: string, ciphertext: string): string {
return sjcl.decrypt(password, ciphertext);
}