lavva.webbluetooth
Version:
Library implementing WebBluetooth custom functionality if underlying platform does support it
257 lines • 10.8 kB
JavaScript
// 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