subtle.mjs raw
1 // TinyJS Runtime — SubtleCrypto bridge (AES-CBC, AES-GCM, PBKDF2, Argon2id, random bytes)
2
3 import { Slice } from './builtin.mjs';
4
5 function sliceToU8(s) {
6 if (s instanceof Uint8Array) return s;
7 const u = new Uint8Array(s.$length);
8 for (let i = 0; i < s.$length; i++) u[i] = s.$array[s.$offset + i];
9 return u;
10 }
11
12 function u8ToSlice(u8) {
13 const arr = new Array(u8.length);
14 for (let i = 0; i < u8.length; i++) arr[i] = u8[i];
15 return new Slice(arr, 0, u8.length, u8.length);
16 }
17
18 function goStringToJS(s) {
19 // Go string in tinyjs is either a JS string or {$val: string}
20 if (typeof s === 'string') return s;
21 if (s && typeof s.$val === 'string') return s.$val;
22 return String(s);
23 }
24
25 export function RandomBytes(dst) {
26 const u8 = new Uint8Array(dst.$length);
27 crypto.getRandomValues(u8);
28 for (let i = 0; i < dst.$length; i++) dst.$array[dst.$offset + i] = u8[i];
29 }
30
31 export function AESCBCEncrypt(key, iv, plaintext, fn) {
32 const k = sliceToU8(key), v = sliceToU8(iv), pt = sliceToU8(plaintext);
33 crypto.subtle.importKey('raw', k, { name: 'AES-CBC' }, false, ['encrypt'])
34 .then(ck => crypto.subtle.encrypt({ name: 'AES-CBC', iv: v }, ck, pt))
35 .then(buf => fn(u8ToSlice(new Uint8Array(buf))))
36 .catch(() => fn(u8ToSlice(new Uint8Array(0))));
37 }
38
39 export function AESCBCDecrypt(key, iv, ciphertext, fn) {
40 const k = sliceToU8(key), v = sliceToU8(iv), ct = sliceToU8(ciphertext);
41 crypto.subtle.importKey('raw', k, { name: 'AES-CBC' }, false, ['decrypt'])
42 .then(ck => crypto.subtle.decrypt({ name: 'AES-CBC', iv: v }, ck, ct))
43 .then(buf => fn(u8ToSlice(new Uint8Array(buf))))
44 .catch(() => fn(u8ToSlice(new Uint8Array(0))));
45 }
46
47 export function AESGCMEncrypt(key, iv, plaintext, fn) {
48 const k = sliceToU8(key), v = sliceToU8(iv), pt = sliceToU8(plaintext);
49 crypto.subtle.importKey('raw', k, { name: 'AES-GCM' }, false, ['encrypt'])
50 .then(ck => crypto.subtle.encrypt({ name: 'AES-GCM', iv: v }, ck, pt))
51 .then(buf => fn(u8ToSlice(new Uint8Array(buf))))
52 .catch(() => fn(u8ToSlice(new Uint8Array(0))));
53 }
54
55 export function AESGCMDecrypt(key, iv, ciphertext, fn) {
56 const k = sliceToU8(key), v = sliceToU8(iv), ct = sliceToU8(ciphertext);
57 crypto.subtle.importKey('raw', k, { name: 'AES-GCM' }, false, ['decrypt'])
58 .then(ck => crypto.subtle.decrypt({ name: 'AES-GCM', iv: v }, ck, ct))
59 .then(buf => fn(u8ToSlice(new Uint8Array(buf))))
60 .catch(() => fn(u8ToSlice(new Uint8Array(0))));
61 }
62
63 export function PBKDF2DeriveKey(password, salt, iterations, fn) {
64 const pw = goStringToJS(password);
65 const s = sliceToU8(salt);
66 const enc = new TextEncoder();
67 crypto.subtle.importKey('raw', enc.encode(pw), 'PBKDF2', false, ['deriveBits'])
68 .then(baseKey => crypto.subtle.deriveBits(
69 { name: 'PBKDF2', salt: s, iterations: iterations, hash: 'SHA-256' },
70 baseKey, 256))
71 .then(bits => fn(u8ToSlice(new Uint8Array(bits))))
72 .catch(() => fn(u8ToSlice(new Uint8Array(0))));
73 }
74
75 export function SHA256Hex(data, fn) {
76 const d = sliceToU8(data);
77 crypto.subtle.digest('SHA-256', d).then(buf => {
78 const arr = new Uint8Array(buf);
79 let hex = '';
80 for (let i = 0; i < arr.length; i++) hex += arr[i].toString(16).padStart(2, '0');
81 fn(hex);
82 }).catch(() => fn(''));
83 }
84
85 // Argon2id — prefers hash-wasm WASM (loaded via UMD in background.html),
86 // falls back to pure-JS argon2.mjs.
87 let _argon2mod = null;
88 export function Argon2idDeriveKey(password, salt, t, m, p, dkLen, fn) {
89 const pw = goStringToJS(password);
90 const s = sliceToU8(salt);
91
92 // hash-wasm WASM path (fast, ~3s for 256MB)
93 if (globalThis.hashwasm && globalThis.hashwasm.argon2id) {
94 globalThis.hashwasm.argon2id({
95 password: pw,
96 salt: s,
97 iterations: t,
98 parallelism: p,
99 memorySize: m,
100 hashLength: dkLen,
101 outputType: 'binary',
102 }).then(result => {
103 fn(u8ToSlice(result));
104 }).catch(e => {
105 console.error('hashwasm Argon2id error:', e);
106 fn(u8ToSlice(new Uint8Array(0)));
107 });
108 return;
109 }
110
111 // Pure-JS fallback (slow, minutes for 256MB)
112 const doDerive = (mod) => {
113 try {
114 const result = mod.argon2id(pw, s, { t, m, p, dkLen });
115 fn(u8ToSlice(result));
116 } catch(e) {
117 console.error('Argon2id error:', e);
118 fn(u8ToSlice(new Uint8Array(0)));
119 }
120 };
121 if (_argon2mod) {
122 doDerive(_argon2mod);
123 } else {
124 import('./argon2.mjs').then(mod => {
125 _argon2mod = mod;
126 doDerive(mod);
127 }).catch(e => {
128 console.error('Failed to load argon2.mjs:', e);
129 fn(u8ToSlice(new Uint8Array(0)));
130 });
131 }
132 }
133
134 // HMACSHA512 computes HMAC-SHA-512 via WebCrypto.
135 // key and data are Go slices. Calls fn with a 64-byte Go slice.
136 export function HMACSHA512(key, data, fn) {
137 const k = sliceToU8(key);
138 const d = sliceToU8(data);
139 crypto.subtle.importKey('raw', k, {name: 'HMAC', hash: 'SHA-512'}, false, ['sign'])
140 .then(ck => crypto.subtle.sign('HMAC', ck, d))
141 .then(buf => fn(u8ToSlice(new Uint8Array(buf))))
142 .catch(() => fn(u8ToSlice(new Uint8Array(0))));
143 }
144
145 // PBKDF2SHA512 derives a key using PBKDF2 with SHA-512 via WebCrypto.
146 // password is a Go string, salt is a Go slice.
147 export function PBKDF2SHA512(password, salt, iterations, dkLen, fn) {
148 const pw = goStringToJS(password);
149 const s = sliceToU8(salt);
150 const enc = new TextEncoder();
151 crypto.subtle.importKey('raw', enc.encode(pw), 'PBKDF2', false, ['deriveBits'])
152 .then(baseKey => crypto.subtle.deriveBits(
153 {name: 'PBKDF2', salt: s, iterations: iterations, hash: 'SHA-512'},
154 baseKey, dkLen * 8))
155 .then(bits => fn(u8ToSlice(new Uint8Array(bits))))
156 .catch(() => fn(u8ToSlice(new Uint8Array(0))));
157 }
158