UNPKG

lavva.webbluetooth

Version:

Library implementing WebBluetooth custom functionality if underlying platform does support it

257 lines 10.8 kB
// LavvaBluetoothEncryptionHelper.ts // Browser-friendly, pure TypeScript ChaCha20 (IETF: 32-byte key, 12-byte nonce, 32-bit counter) export const toByte = (n) => (n & 0xff); export class LavvaBluetoothEncryptionHelper { // --- Public API (key/nonce derivation you already had) --- /** Parse MAC like "AA:BB:CC:DD:EE:FF" or "AABBCCDDEEFF" into 6 bytes */ static parseMac(macStr) { const mac = new Uint8Array(6); let out = 0; let hi = -1; for (let i = 0; i < macStr.length; i++) { const c = macStr.charCodeAt(i); if (c === 58 /*:*/ || c === 45 /*-*/) continue; const n = LavvaBluetoothEncryptionHelper.hexNibble(c); if (n < 0) throw new Error("Invalid MAC address format"); if (hi < 0) { hi = n; } else { if (out >= 6) throw new Error("Invalid MAC address format"); mac[out++] = (hi << 4) | n; hi = -1; } } if (out !== 6 || hi !== -1) throw new Error("Invalid MAC address format"); return mac; } /** Your MAC->key derivation (32 bytes) */ static calcChacha20Key(macStr) { const mac = LavvaBluetoothEncryptionHelper.parseMac(macStr); const key = new Uint8Array(32); let step = mac[5] % 6; if (step === 0) step = 2; const operations = [ (a, b) => toByte(a & b), (a, b) => toByte(a | b), (a, b) => toByte(a ^ b), (a, _b) => toByte(~a), (a, b) => toByte(~(a ^ b)), ]; key[0] = 0x4c; // 'L' // precompute (j + step) % 6 const idx = new Uint8Array(6); for (let j = 0; j < 6; j++) idx[j] = (j + step) % 6; for (let i = 0; i < 5; i++) { const op = operations[i]; const base = i * 6 + 1; for (let j = 0; j < 6; j++) { const a = mac[j]; // Uint8Array => runtime 0..255 const b = mac[idx[j]]; // jw. key[base + j] = op(a, b); } } key[31] = 0x61; // 'a' return key; } /** 12-byte nonce from payload (little-endian uint32 x3) */ static getNonce({ validFrom, sessionId, validTo }) { const nonce = new Uint8Array(12); const dv = new DataView(nonce.buffer, nonce.byteOffset, nonce.byteLength); dv.setUint32(0, validFrom >>> 0, true); dv.setUint32(4, sessionId >>> 0, true); dv.setUint32(8, validTo >>> 0, true); return nonce; } // --- Public API (ChaCha20 encrypt/decrypt) --- /** * ChaCha20 stream cipher (IETF): XOR plaintext with keystream. * Default counter=1 (common when reserving counter=0 for Poly1305 key derivation). */ static chacha20EncryptBytes(key32, nonce12, plaintext, counter = 1) { return LavvaBluetoothEncryptionHelper.chacha20Xor(key32, nonce12, plaintext, counter); } /** Decryption is identical (XOR again) */ static chacha20DecryptBytes(key32, nonce12, ciphertext, counter = 1) { return LavvaBluetoothEncryptionHelper.chacha20Xor(key32, nonce12, ciphertext, counter); } /** Encrypt hex string (bytes in hex) -> ciphertext hex */ static chacha20EncryptHex(key32, nonce12, plaintextHex, counter = 1) { const pt = LavvaBluetoothEncryptionHelper.hexToBytes(plaintextHex); const ct = LavvaBluetoothEncryptionHelper.chacha20EncryptBytes(key32, nonce12, pt, counter); return LavvaBluetoothEncryptionHelper.bytesToHex(ct); } /** Decrypt hex string (ciphertext hex) -> plaintext hex */ static chacha20DecryptHex(key32, nonce12, ciphertextHex, counter = 1) { const ct = LavvaBluetoothEncryptionHelper.hexToBytes(ciphertextHex); const pt = LavvaBluetoothEncryptionHelper.chacha20DecryptBytes(key32, nonce12, ct, counter); return LavvaBluetoothEncryptionHelper.bytesToHex(pt); } constructor(macStr) { this.key32 = LavvaBluetoothEncryptionHelper.calcChacha20Key(macStr); } encryptHexWithSession(payload, plaintextHex, counter = 1) { const nonce = LavvaBluetoothEncryptionHelper.getNonce(payload); return LavvaBluetoothEncryptionHelper.chacha20EncryptHex(this.key32, nonce, plaintextHex, counter); } decryptHexWithSession(payload, ciphertextHex, counter = 1) { const nonce = LavvaBluetoothEncryptionHelper.getNonce(payload); return LavvaBluetoothEncryptionHelper.chacha20DecryptHex(this.key32, nonce, ciphertextHex, counter); } getKeyBytes() { return new Uint8Array(this.key32); } static chacha20Xor(key32, nonce12, input, counter) { if (key32.length !== 32) throw new Error("ChaCha20 key must be 32 bytes"); if (nonce12.length !== 12) throw new Error("ChaCha20 nonce must be 12 bytes"); const out = new Uint8Array(input.length); let ctr = counter >>> 0; const block = new Uint8Array(64); let offset = 0; while (offset < input.length) { LavvaBluetoothEncryptionHelper.chacha20Block(block, key32, nonce12, ctr); ctr = (ctr + 1) >>> 0; const n = Math.min(64, input.length - offset); for (let i = 0; i < n; i++) out[offset + i] = input[offset + i] ^ block[i]; offset += n; } return out; } /** Write 64-byte keystream block into `out64` */ static chacha20Block(out64, key32, nonce12, counter) { // state: 16 x u32 const state = new Uint32Array(16); // constants state[0] = LavvaBluetoothEncryptionHelper.SIGMA[0]; state[1] = LavvaBluetoothEncryptionHelper.SIGMA[1]; state[2] = LavvaBluetoothEncryptionHelper.SIGMA[2]; state[3] = LavvaBluetoothEncryptionHelper.SIGMA[3]; // key (8 words LE) for (let i = 0; i < 8; i++) { state[4 + i] = LavvaBluetoothEncryptionHelper.u8ToU32LE(key32, i * 4); } // counter state[12] = counter >>> 0; // nonce (3 words LE) state[13] = LavvaBluetoothEncryptionHelper.u8ToU32LE(nonce12, 0); state[14] = LavvaBluetoothEncryptionHelper.u8ToU32LE(nonce12, 4); state[15] = LavvaBluetoothEncryptionHelper.u8ToU32LE(nonce12, 8); // working copy const working = new Uint32Array(state); // 20 rounds = 10 double-rounds for (let i = 0; i < 10; i++) { // column rounds LavvaBluetoothEncryptionHelper.quarterRound(working, 0, 4, 8, 12); LavvaBluetoothEncryptionHelper.quarterRound(working, 1, 5, 9, 13); LavvaBluetoothEncryptionHelper.quarterRound(working, 2, 6, 10, 14); LavvaBluetoothEncryptionHelper.quarterRound(working, 3, 7, 11, 15); // diagonal rounds LavvaBluetoothEncryptionHelper.quarterRound(working, 0, 5, 10, 15); LavvaBluetoothEncryptionHelper.quarterRound(working, 1, 6, 11, 12); LavvaBluetoothEncryptionHelper.quarterRound(working, 2, 7, 8, 13); LavvaBluetoothEncryptionHelper.quarterRound(working, 3, 4, 9, 14); } // add original state for (let i = 0; i < 16; i++) working[i] = (working[i] + state[i]) >>> 0; // serialize LE for (let i = 0; i < 16; i++) { LavvaBluetoothEncryptionHelper.u32ToU8LE(working[i], out64, i * 4); } } static quarterRound(s, a, b, c, d) { s[a] = (s[a] + s[b]) >>> 0; s[d] ^= s[a]; s[d] = LavvaBluetoothEncryptionHelper.rotl32(s[d], 16); s[c] = (s[c] + s[d]) >>> 0; s[b] ^= s[c]; s[b] = LavvaBluetoothEncryptionHelper.rotl32(s[b], 12); s[a] = (s[a] + s[b]) >>> 0; s[d] ^= s[a]; s[d] = LavvaBluetoothEncryptionHelper.rotl32(s[d], 8); s[c] = (s[c] + s[d]) >>> 0; s[b] ^= s[c]; s[b] = LavvaBluetoothEncryptionHelper.rotl32(s[b], 7); } static rotl32(x, n) { return ((x << n) | (x >>> (32 - n))) >>> 0; } static u8ToU32LE(u8, off) { return ((u8[off] | (u8[off + 1] << 8) | (u8[off + 2] << 16) | (u8[off + 3] << 24)) >>> 0); } static u32ToU8LE(x, out, off) { out[off] = x & 0xff; out[off + 1] = (x >>> 8) & 0xff; out[off + 2] = (x >>> 16) & 0xff; out[off + 3] = (x >>> 24) & 0xff; } // --- Internal: hex utils --- static bytesToHex(bytes) { // szybciej niż toString(16) w pętli const lut = LavvaBluetoothEncryptionHelper.HEX_LUT; let out = ""; for (let i = 0; i < bytes.length; i++) out += lut[bytes[i]]; return out; } static hexToBytes(hex) { let clean = hex.trim().replace(/^0x/i, "").replace(/\s+/g, ""); if (clean.length % 2 !== 0) throw new Error("Invalid hex string length"); const out = new Uint8Array(clean.length / 2); for (let i = 0; i < out.length; i++) { const hi = LavvaBluetoothEncryptionHelper.hexNibble(clean.charCodeAt(i * 2)); const lo = LavvaBluetoothEncryptionHelper.hexNibble(clean.charCodeAt(i * 2 + 1)); if (hi < 0 || lo < 0) throw new Error("Invalid hex string"); out[i] = (hi << 4) | lo; } return out; } static hexNibble(code) { if (code >= 48 && code <= 57) return code - 48; // 0-9 if (code >= 65 && code <= 70) return code - 55; // A-F if (code >= 97 && code <= 102) return code - 87; // a-f return -1; } // UTF-8 string -> hex (UTF-8 bytes zapisane jako hex) static stringToHex(str) { const bytes = new TextEncoder().encode(str); return LavvaBluetoothEncryptionHelper.bytesToHex(bytes); } // hex -> UTF-8 string (hex -> bytes -> decode UTF-8) static hexToString(hex) { const bytes = LavvaBluetoothEncryptionHelper.hexToBytes(hex); // fatal:true => rzuć błąd przy niepoprawnym UTF-8 (opcjonalnie) return new TextDecoder("utf-8", { fatal: true }).decode(bytes); } } // --- Internal: ChaCha20 core (RFC/IETF layout) --- LavvaBluetoothEncryptionHelper.SIGMA = new Uint32Array([ 0x61707865, // "expa" 0x3320646e, // "nd 3" 0x79622d32, // "2-by" 0x6b206574, // "te k" ]); LavvaBluetoothEncryptionHelper.HEX_LUT = (() => { const lut = new Array(256); for (let i = 0; i < 256; i++) lut[i] = i.toString(16).padStart(2, "0"); return lut; })(); //# sourceMappingURL=LavvaBluetoothEncryptionHelper.js.map