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