boho
Version:
Secure Lightweight Encryption & Authentication Library for Node.js, Browsers and Arduino.
529 lines (452 loc) • 11.8 kB
JavaScript
// 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
}
}