UNPKG

boho

Version:

Secure Lightweight Encryption & Authentication Library for Node.js, Browsers and Arduino.

529 lines (452 loc) 11.8 kB
// Copyright (c) 2024 Taeo Lee (sixgen@gmail.com) // MIT License // // https://github.com/remocons/boho // // // import { sha256 } from './sha256-mbp.js' import MBP from 'meta-buffer-pack' export { MBP } import { BohoMsg, Meta, MetaSize } from './constants.js' export { BohoMsg, Meta, MetaSize, sha256 } import { Buffer } from 'buffer/index.js' export { Buffer } /** * Generates a random byte buffer. * @param {number} size - Number of bytes to generate * @returns {Buffer} */ export function RAND(size) { return globalThis.crypto.getRandomValues(Buffer.alloc(size)) } /** * Boho security protocol class */ export class Boho { /** * @constructor */ constructor() { this._id8 = Buffer.alloc(8) this._otpSrc44 = Buffer.alloc(44) this._otp36 = Buffer.alloc(36) this._hmac = Buffer.alloc(32) this.auth_salt12 = Buffer.alloc(12) this.localNonce = Buffer.alloc(4) this.remoteNonce = Buffer.alloc(4) this.isAuthorized = false this.counter = 0; } /** * Initializes authentication state. */ clearAuth() { this._id8.fill(0) this._otpSrc44.fill(0) this._otp36.fill(0) this._hmac.fill(0) this.auth_salt12.fill(0) this.localNonce.fill(0) this.remoteNonce.fill(0) this.isAuthorized = false this.counter = 0; } /** * Sets id8 by hashing the input data. * @param {any} data */ set_hash_id8(data) { let idSum = MBP.B8(sha256.hash(data)) idSum.copy(this._id8, 0, 0, 8) } /** * Sets the id8 value. * @param {any} data */ set_id8(data) { let encStr = MBP.B8(data) this._id8.fill(0) encStr.copy(this._id8, 0, 0, 8) } /** * Sets the key value by hashing and storing in otpSrc44. * @param {any} data */ set_key(data) { let keySum = MBP.B8(sha256.hash(data)) keySum.copy(this._otpSrc44, 0, 0, 32) } /** * Splits 'id.key' string and sets id8 and key. * @param {string} id_key */ set_id_key(id_key) { let delimiterPosition = id_key.indexOf('.') if (delimiterPosition == -1) return let id = id_key.substring(0, delimiterPosition) let key = id_key.substring(delimiterPosition + 1) this.set_id8(id) this.set_key(key) } /** * Copies id8 value from external buffer. * @param {Buffer} data */ copy_id8(data) { data.copy(this._id8, 0, 0, 8) } /** * Copies key value from external buffer. * @param {Buffer} data */ copy_key(data) { data.copy(this._otpSrc44, 0, 0, 32) } /** * Applies sha256 hash n times. * @param {any} srcData * @param {number} n * @returns {Uint8Array} */ sha256_n(srcData, n) { let hashSum = sha256.hash(srcData) for (let i = 0; i < n; i++) hashSum = sha256.hash(hashSum) return hashSum } /** * Sets random clock value (salt12) in otpSrc44. */ set_clock_rand() { const now = Date.now() const secTime = parseInt(now / 1000) const milTime = now % 1000 if (++this.counter > 65535) this.counter = 0; const salt12 = Buffer.concat([ MBP.NB('32L', secTime), MBP.NB('16L', milTime), MBP.NB('16L', this.counter), RAND(4) // JS: use crypto.getRandomValues(), Arduino: use micros() ]) salt12.copy(this._otpSrc44, 32) } /** * Sets otpSrc44 with given nonce and clock value. * @param {Buffer} nonce */ set_clock_nonce(nonce) { const now = Date.now() const secTime = parseInt(now / 1000) const milTime = now % 1000 if (++this.counter > 65535) this.counter = 0; const salt12 = Buffer.concat([ MBP.NB('32L', secTime), MBP.NB('16L', milTime), MBP.NB('16L', this.counter), nonce ]) salt12.copy(this._otpSrc44, 32) } /** * Sets salt12 value in otpSrc44. * @param {Buffer} salt12 */ set_salt12(salt12 , caller) { // console.log('boho.set_salt12', salt12 , caller ) if( salt12.byteLength != 12){ throw TypeError('set_salt12: Invalid salt12 byteLength.') } salt12.copy(this._otpSrc44, 32) } /** * Initializes OTP value. */ resetOTP() { let otp32 = MBP.B8(sha256.hash(this._otpSrc44)) otp32.copy(this._otp36, 0, 0, 32) } /** * Returns OTP value for the given index. * @param {number} otpIndex * @returns {Uint8Array} */ getIndexOTP(otpIndex) { this._otp36.writeUInt32LE(otpIndex, 32) return sha256.hash(this._otp36) } /** * Generates HMAC value. * @param {Buffer} data */ generateHMAC(data) { let hmacSrc = Buffer.concat([this._otpSrc44, data]) this._hmac = MBP.B8(sha256.hash(hmacSrc)) } /** * Returns 8-byte HMAC value. * @param {Buffer} data * @returns {Buffer} */ getHMAC8(data) { let hmacSrc = Buffer.concat([this._otpSrc44, data]) this._hmac = MBP.B8(sha256.hash(hmacSrc)) return this._hmac.subarray(0, 8) } /** * OTP-based XOR encryption/decryption * @param {Buffer} data * @param {number} [otpStartIndex=0] * @param {boolean} [shareDataBuffer=false] * @returns {Buffer} */ xotp(data, otpStartIndex = 0, shareDataBuffer = false) { data = MBP.B8(data, shareDataBuffer) let len = data.byteLength let otpIndex = otpStartIndex let dataOffset = 0 let xorCalcLen = 0 while (len > 0) { xorCalcLen = len < 32 ? len : 32 let iotp = this.getIndexOTP(++otpIndex); for (let i = 0; i < xorCalcLen; i++) { data[dataOffset++] ^= iotp[i] } len -= 32 } return data } // B. AUTH process /** * Generates server time and nonce signal * @returns {Buffer} */ server_time_nonce() { const now = Date.now() const unixTime = Math.floor(now / 1000) const milliseconds = now % 1000; this.localNonce = RAND(4) //keep this.auth_salt12 = Buffer.concat([ MBP.NB('32L', unixTime), MBP.NB('16L', milliseconds), RAND(2), // used for client's counter IV. this.localNonce ]) let infoPack = Buffer.concat([ MBP.NB('8', BohoMsg.SERVER_TIME_NONCE), this.auth_salt12 ]) return infoPack } /** * Generates AUTH_REQ message * @param {Buffer} buffer server's time nonce * @returns {Buffer|boolean} */ auth_req(buffer) { let server_time_nonce = MBP.unpack(buffer, Meta.SERVER_TIME_NONCE) if (server_time_nonce) { let salt12 = Buffer.concat([ MBP.NB('32L', server_time_nonce.unixTime), MBP.NB('16L', server_time_nonce.milTime), MBP.NB('16L', server_time_nonce.counter), server_time_nonce.nonce ]) this.set_salt12(salt12, 'auth_req') this.localNonce = RAND(4) this.generateHMAC(this.localNonce) this.remoteNonce = server_time_nonce.nonce let auth_hmac_buffer = MBP.pack( MBP.MB('#header', '8', BohoMsg.AUTH_REQ), MBP.MB('#id8', this._id8), MBP.MB('#nonce', this.localNonce), MBP.MB('#hmac32', this._hmac), ) return auth_hmac_buffer } return false } /* step 4. for server */ /** * Verify client's AUTH_REQ. * @param {Buffer|object} data * @returns {boolean} */ verify_auth_req(data) { let infoPack if (data instanceof Uint8Array) { infoPack = MBP.unpack(data, Meta.AUTH_REQ) if (!infoPack) { return } } else { infoPack = data; } this.set_salt12(this.auth_salt12, 'verify_auth_req 1/2') this.generateHMAC(infoPack.nonce) let hmac32 = this._hmac if (MBP.equal(infoPack.hmac32, hmac32)) { this.remoteNonce = infoPack.nonce let salt12 = Buffer.concat([ this.localNonce, this.remoteNonce, this.localNonce ]) this.set_salt12(salt12, 'verify_auth_req 2/2') this.generateHMAC(infoPack.nonce) let replyHMAC = this._hmac let auth_res = MBP.rawPack( MBP.MB('header', '8', BohoMsg.AUTH_RES), MBP.MB('hmac32', replyHMAC) ) this.isAuthorized = true return auth_res } return false } /** * Verifies server's AUTH_RES HMAC. * @param {Buffer} buffer * @returns {boolean} */ verify_auth_res(buffer) { let auth_res = MBP.unpack(buffer, Meta.AUTH_RES) if (auth_res) { let salt12 = Buffer.concat([ this.remoteNonce, this.localNonce, this.remoteNonce, ]) this.set_salt12(salt12, 'verify_auth_res') this.generateHMAC(this.localNonce) let hmac32 = this._hmac if (MBP.equal(hmac32, auth_res.hmac32)) { this.isAuthorized = true return true } } return } // C. Secure Communication // Must AUTH first. /** * Generates encrypted 488 packet after authentication * @param {Buffer} data * @returns {Buffer|undefined} */ encrypt_488(data) { if (!this.isAuthorized) return data = MBP.B8(data) this.set_clock_nonce(this.remoteNonce) this.resetOTP() let hmac8 = this.getHMAC8(data) let encData = this.xotp(data) let pack = MBP.pack( MBP.MB('#type', '8', BohoMsg.ENC_488), MBP.MB('#len', '32L', data.byteLength), MBP.MB('#otpSrc8', this._otpSrc44.subarray(32, 40)), MBP.MB('#hmac8', hmac8), MBP.MB('#xdata', encData) ) return pack } /** * Decrypts 488 packet after authentication * @param {Buffer} data * @returns {Buffer|undefined} */ decrypt_488(data) { data = MBP.B8(data) let pack = MBP.unpack(data, Meta.ENC_488) if (pack) { let salt12 = Buffer.concat([ pack.otpSrc8, this.localNonce ]) this.set_salt12(salt12 , 'decrypt_488') this.resetOTP() let xdata = pack.$OTHERS.subarray(0, pack.len) let decData = this.xotp(xdata) let hmac8 = this.getHMAC8(decData) if (MBP.equal(hmac8, pack.hmac8)) return decData } } /** * Generates encrypted packet for up to 2^32-1 bytes * @param {Buffer} data * @returns {Buffer} */ encryptPack(data) { data = MBP.B8(data) this.set_clock_rand() this.resetOTP() let hmac8 = this.getHMAC8(data) let encData = this.xotp(data) let pack = MBP.pack( MBP.MB('#type', '8', BohoMsg.ENC_PACK), MBP.MB('#len', '32L', data.byteLength), MBP.MB('#salt12', this._otpSrc44.subarray(32)), MBP.MB('#hmac8', hmac8), MBP.MB('#xdata', encData) ) return pack } /** * Decrypts encrypted packet * @param {Buffer} data * @returns {Buffer} */ decryptPack(data) { if (data[0] !== BohoMsg.ENC_PACK) { return } let readPackLen = data.readUint32LE(1); if (readPackLen != data.byteLength - MetaSize.ENC_PACK) { return } try { let pack = MBP.unpack(data, Meta.ENC_PACK) if (!pack) return this.set_salt12(pack.salt12, 'decryptPack') this.resetOTP() let xdata = pack.$OTHERS let decData = this.xotp(xdata) let hmac8 = this.getHMAC8(decData) if (MBP.equal(pack.hmac, hmac8)) { pack.data = decData return pack } } catch (error) { } } /** * End-to-end encryption * @param {Buffer} data * @param {Buffer} key * @returns {Buffer} */ encrypt_e2e(data, key) { let baseKey = Buffer.alloc(32) baseKey.set(this._otpSrc44.subarray(0, 32)) this.set_key(key) let pack = this.encryptPack(data) this._otpSrc44.set(baseKey) return pack; } /** * End-to-end decryption * @param {Buffer} data * @param {Buffer} key * @returns {Buffer} */ decrypt_e2e(data, key) { let baseKey = Buffer.alloc(32) baseKey.set(this._otpSrc44.subarray(0, 32)) this.set_key(key) let decPack = this.decryptPack(data) this._otpSrc44.set(baseKey) return decPack } }