inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
184 lines (157 loc) • 4.63 kB
text/typescript
/** Management class and utils for Security S0 */
import { randomBytes } from "crypto";
import { ZWaveError, ZWaveErrorCodes } from "../error/ZWaveError";
import { encryptAES128ECB } from "./crypto";
const authKeyBase = Buffer.alloc(16, 0x55);
const encryptionKeyBase = Buffer.alloc(16, 0xaa);
export function generateAuthKey(networkKey: Buffer): Buffer {
return encryptAES128ECB(authKeyBase, networkKey);
}
export function generateEncryptionKey(networkKey: Buffer): Buffer {
return encryptAES128ECB(encryptionKeyBase, networkKey);
}
interface NonceKey {
/** The node that has created this nonce */
issuer: number;
nonceId: number;
}
interface NonceEntry {
nonce: Buffer;
/** The node this nonce was created for */
receiver: number;
}
export interface SecurityManagerOptions {
networkKey: Buffer;
ownNodeId: number;
nonceTimeout: number;
}
export interface SetNonceOptions {
free?: boolean;
}
export class SecurityManager {
public constructor(options: SecurityManagerOptions) {
this.networkKey = options.networkKey;
this.ownNodeId = options.ownNodeId;
this.nonceTimeout = options.nonceTimeout;
}
private ownNodeId: number;
private nonceTimeout: number;
private _networkKey!: Buffer;
public get networkKey(): Buffer {
return this._networkKey;
}
public set networkKey(v: Buffer) {
if (v.length !== 16) {
throw new ZWaveError(
`The network key must be 16 bytes long!`,
ZWaveErrorCodes.Argument_Invalid,
);
}
this._networkKey = v;
this._authKey = generateAuthKey(this._networkKey);
this._encryptionKey = generateEncryptionKey(this._networkKey);
}
private _authKey!: Buffer;
public get authKey(): Buffer {
return this._authKey;
}
private _encryptionKey!: Buffer;
public get encryptionKey(): Buffer {
return this._encryptionKey;
}
private _nonceStore = new Map<string, NonceEntry>();
private _freeNonceIDs = new Set<string>();
private _nonceTimers = new Map<string, NodeJS.Timeout>();
private normalizeId(id: number | NonceKey): string {
let ret: NonceKey;
if (typeof id === "number") {
ret = {
issuer: this.ownNodeId,
nonceId: id,
};
} else {
ret = {
issuer: id.issuer,
nonceId: id.nonceId,
};
}
return JSON.stringify(ret);
}
/** Generates a nonce for the current node */
public generateNonce(receiver: number, length: number): Buffer {
let nonce: Buffer;
let nonceId: number;
do {
nonce = randomBytes(length);
nonceId = this.getNonceId(nonce);
} while (this.hasNonce(nonceId));
this.setNonce(nonceId, { receiver, nonce }, { free: false });
return nonce;
}
public getNonceId(nonce: Buffer): number {
return nonce[0];
}
public setNonce(
id: number | NonceKey,
entry: NonceEntry,
{ free = true }: SetNonceOptions = {},
): void {
const key = this.normalizeId(id);
if (this._nonceTimers.has(key)) {
clearTimeout(this._nonceTimers.get(key));
}
this._nonceStore.set(key, entry);
if (free) this._freeNonceIDs.add(key);
this._nonceTimers.set(
key,
setTimeout(() => {
this.expireNonce(key);
}, this.nonceTimeout).unref(),
);
}
/** Deletes ALL nonces that were issued for a given node */
public deleteAllNoncesForReceiver(receiver: number): void {
for (const [key, entry] of this._nonceStore) {
if (entry.receiver === receiver) {
this.deleteNonceInternal(key);
}
}
}
public deleteNonce(id: number | NonceKey): void {
const key = this.normalizeId(id);
const nonceReceiver = this._nonceStore.get(key)?.receiver;
// Delete the nonce that was requested to be deleted
this.deleteNonceInternal(key);
// And all others for the same receiver aswell
if (nonceReceiver) {
this.deleteAllNoncesForReceiver(nonceReceiver);
}
}
private deleteNonceInternal(key: string) {
if (this._nonceTimers.has(key)) {
clearTimeout(this._nonceTimers.get(key));
}
this._nonceStore.delete(key);
this._nonceTimers.delete(key);
this._freeNonceIDs.delete(key);
}
private expireNonce(key: string): void {
this.deleteNonceInternal(key);
}
public getNonce(id: number | NonceKey): Buffer | undefined {
return this._nonceStore.get(this.normalizeId(id))?.nonce;
}
public hasNonce(id: number | NonceKey): boolean {
return this._nonceStore.has(this.normalizeId(id));
}
public getFreeNonce(nodeId: number): Buffer | undefined {
// Iterate through the known free nonce IDs to find one for the given node
for (const key of this._freeNonceIDs) {
const nonceKey = JSON.parse(key) as NonceKey;
if (nonceKey.issuer === nodeId) {
this._freeNonceIDs.delete(key);
return this._nonceStore.get(key)?.nonce;
}
}
}
}