UNPKG

knxultimate

Version:

KNX IP protocol implementation for Node. This is the ENGINE of Node-Red KNX-Ultimate node.

169 lines (147 loc) 4.65 kB
/** * Implements KNX Secure cryptographic primitives. * * Written in Italy with love, sun and passion, by Massimo Saccani. * * Released under the MIT License. * Use at your own risk; the author assumes no liability for damages. */ /** * Encryption and Decryption functions for KNX Secure */ import * as crypto from 'crypto' import { bytePad } from './util' /** * Calculate the message authentication code (MAC) for a message with AES-CBC * @param key - 16-byte AES key * @param additionalData - Additional data to be included in the MAC calculation * @param payload - Optional payload data * @param block0 - Optional block 0 (defaults to 16 zero bytes) * @returns MAC (16 bytes) */ export function calculateMessageAuthenticationCodeCBC( key: Buffer, additionalData: Buffer, payload: Buffer = Buffer.alloc(0), block0: Buffer = Buffer.alloc(16), ): Buffer { // Concatenate block0 + additional data length (2 bytes) + additional data + payload const additionalDataLength = Buffer.alloc(2) additionalDataLength.writeUInt16BE(additionalData.length, 0) // Use Buffer.from for better type compatibility const blocks = Buffer.from( Buffer.concat([block0, additionalDataLength, additionalData, payload]), ) // Apply padding to ensure blocks are multiple of 16 bytes const paddedBlocks = bytePad(blocks, 16) // Use AES-CBC with zero IV const iv = Buffer.alloc(16) // Use key.slice(0, 16) to ensure key is exactly 16 bytes const cipher = crypto.createCipheriv('aes-128-cbc', key.slice(0, 16), iv) cipher.setAutoPadding(false) // We've already padded manually const encrypted = Buffer.from( Buffer.concat([cipher.update(paddedBlocks), cipher.final()]), ) // Return last 16 bytes (MAC) return Buffer.from(encrypted.slice(encrypted.length - 16)) } /** * Decrypt data from SecureWrapper * @param key - 16-byte AES key * @param counter0 - Initial counter value (16 bytes) * @param mac - MAC value (16 bytes) * @param payload - Encrypted payload * @returns Tuple of [decrypted data, MAC-TR for verification] */ export function decryptCtr( key: Buffer, counter0: Buffer, mac: Buffer, payload: Buffer = Buffer.alloc(0), ): [Buffer, Buffer] { // For AES-CTR in Node.js, we need to ensure the counter is properly formatted const cipher = crypto.createDecipheriv( 'aes-128-ctr', key.slice(0, 16), counter0.slice(0, 16), ) // MAC is encrypted with counter 0 const macTr = Buffer.from(cipher.update(mac)) // Decrypt the payload const decryptedData = Buffer.from( Buffer.concat([cipher.update(payload), cipher.final()]), ) return [decryptedData, macTr] } /** * Encrypt data with AES-CTR * @param key - 16-byte AES key * @param counter0 - Initial counter value (16 bytes) * @param macCbc - CBC-MAC value (16 bytes) * @param payload - Data to encrypt * @returns Tuple of [encrypted payload, encrypted MAC] */ export function encryptDataCtr( key: Buffer, counter0: Buffer, macCbc: Buffer, payload: Buffer = Buffer.alloc(0), ): [Buffer, Buffer] { // Use AES-CTR for encryption const cipher = crypto.createCipheriv( 'aes-128-ctr', key.slice(0, 16), counter0.slice(0, 16), ) // Encrypt MAC with counter 0 const encryptedMac = Buffer.from(cipher.update(macCbc)) // Encrypt the payload const encryptedData = Buffer.from( Buffer.concat([cipher.update(payload), cipher.final()]), ) return [encryptedData, encryptedMac] } /** * Derive device authentication password using PBKDF2 * @param deviceAuthenticationPassword - Password string * @returns 16-byte derived key */ export function deriveDeviceAuthenticationPassword( deviceAuthenticationPassword: string, ): Buffer { return crypto.pbkdf2Sync( Buffer.from(deviceAuthenticationPassword, 'latin1'), 'device-authentication-code.1.secure.ip.knx.org', 65536, 16, 'sha256', ) } /** * Derive user password using PBKDF2 * @param passwordString - Password string * @returns 16-byte derived key */ export function deriveUserPassword(passwordString: string): Buffer { return crypto.pbkdf2Sync( Buffer.from(passwordString, 'latin1'), 'user-password.1.secure.ip.knx.org', 65536, 16, 'sha256', ) } /** * Generate an ECDH key pair using X25519 * @returns Tuple of [privateKey, publicKey] */ export function generateEcdhKeyPair(): [crypto.KeyObject, Buffer] { // Generate private key const privateKey = crypto.generateKeyPairSync('x25519').privateKey // Export public key - Note: raw format is not directly supported in types, using 'as any' const publicKey = crypto.createPublicKey(privateKey).export({ format: 'der', type: 'spki', }) return [privateKey, Buffer.from(publicKey)] }