UNPKG

zwave-js

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

340 lines 15.6 kB
import { CommandClass, ECDHProfiles, InvalidCC, KEXSchemes, Security2CC, Security2CCCommandsSupportedGet, Security2CCCommandsSupportedReport, Security2CCKEXGet, Security2CCKEXReport, Security2CCKEXSet, Security2CCMessageEncapsulation, Security2CCNetworkKeyGet, Security2CCNetworkKeyReport, Security2CCNetworkKeyVerify, Security2CCNonceGet, Security2CCNonceReport, Security2CCPublicKeyReport, Security2CCTransferEnd, Security2Command, } from "@zwave-js/cc"; import { CommandClasses, EncapsulationFlags, ZWaveErrorCodes, computePRK, deriveSharedECDHSecret, deriveTempKeys, } from "@zwave-js/core"; import { MockZWaveFrameType, createMockZWaveRequestFrame, } from "@zwave-js/testing"; var BootstrapStage; (function (BootstrapStage) { BootstrapStage[BootstrapStage["Started"] = 0] = "Started"; BootstrapStage[BootstrapStage["Handshake"] = 1] = "Handshake"; BootstrapStage[BootstrapStage["RequestingKeys"] = 2] = "RequestingKeys"; BootstrapStage[BootstrapStage["VerifyingKeys"] = 3] = "VerifyingKeys"; BootstrapStage[BootstrapStage["Finalizing"] = 4] = "Finalizing"; })(BootstrapStage || (BootstrapStage = {})); const STATE_KEY_PREFIX = "Security2_"; const StateKeys = { bootstrapState: `${STATE_KEY_PREFIX}bootstrapState`, }; const respondToS2KEXGet = { handleCC(controller, self, receivedCC) { const sm2Node = self.securityManagers.securityManager2; if (!sm2Node) return; if (receivedCC instanceof Security2CCKEXGet) { // Reset the bootstrap state self.state.set(StateKeys.bootstrapState, { stage: BootstrapStage.Started }); const cc = new Security2CCKEXReport({ nodeId: controller.ownNodeId, echo: false, supportedKEXSchemes: [KEXSchemes.KEXScheme1], supportedECDHProfiles: [ECDHProfiles.Curve25519], requestCSA: false, requestedKeys: [...self.capabilities.securityClasses], }); return { action: "sendCC", cc }; } }, }; const respondToS2KEXSet = { handleCC(controller, self, receivedCC) { const sm2Node = self.securityManagers.securityManager2; if (!sm2Node) return; if (receivedCC instanceof Security2CCKEXSet && !receivedCC.echo) { // Remember the granted keys self.state.set(StateKeys.bootstrapState, { stage: BootstrapStage.Handshake, grantedKeys: receivedCC.grantedKeys, }); // We're supposed to respond with our public key now const cc = new Security2CCPublicKeyReport({ nodeId: controller.ownNodeId, includingNode: false, publicKey: self.ecdhKeyPair.publicKey, }); return { action: "sendCC", cc }; } }, }; const respondToS2KEXReport = { async handleCC(controller, self, receivedCC) { const sm2Node = self.securityManagers.securityManager2; if (!sm2Node) return; if (receivedCC instanceof Security2CCKEXReport && receivedCC.echo && receivedCC.isEncapsulatedWith(CommandClasses["Security 2"])) { // This should happen after the "handshake" const currentState = self.state.get(StateKeys.bootstrapState); if (currentState?.stage !== BootstrapStage.Handshake) return; // We are now ready to request the network keys self.state.set(StateKeys.bootstrapState, { stage: BootstrapStage.RequestingKeys, grantedKeys: currentState.grantedKeys, keys: new Map(), }); // Kick off the key request process using the first key const firstKey = currentState.grantedKeys[0]; return requestKey(self, firstKey); } return undefined; }, }; const handleS2PublicKeyReport = { async handleCC(controller, self, receivedCC) { const sm2Node = self.securityManagers.securityManager2; if (!sm2Node) return; if (receivedCC instanceof Security2CCPublicKeyReport && receivedCC.includingNode) { // Derive the shared secret and temp keys const sharedSecret = await deriveSharedECDHSecret({ publicKey: receivedCC.publicKey, privateKey: self.ecdhKeyPair.privateKey, }); const tempKeys = await deriveTempKeys(await computePRK(sharedSecret, receivedCC.publicKey, self.ecdhKeyPair.publicKey)); sm2Node.deleteNonce(controller.ownNodeId); sm2Node.tempKeys.set(controller.ownNodeId, { keyCCM: tempKeys.tempKeyCCM, personalizationString: tempKeys.tempPersonalizationString, }); // The mock node does not have the magic for automatically // initializing the SPAN state, so we have to do it ourselves here. // This requires a nonce exchange. await requestNonce(controller, self); // Send the KEX Set echo to finalize creation of the secure channel let cc = new Security2CCKEXSet({ nodeId: controller.ownNodeId, echo: true, // TODO: We should copy these from the received KEX Set instead selectedKEXScheme: KEXSchemes.KEXScheme1, selectedECDHProfile: ECDHProfiles.Curve25519, permitCSA: false, grantedKeys: [...self.capabilities.securityClasses], }); cc = Security2CC.encapsulate(cc, self.id, self.securityManagers); return { action: "sendCC", cc }; } }, }; const handleS2NetworkKeyReport = { async handleCC(controller, self, receivedCC) { const sm2Node = self.securityManagers.securityManager2; if (!sm2Node) return; if (receivedCC instanceof Security2CCNetworkKeyReport && receivedCC.isEncapsulatedWith(CommandClasses["Security 2"])) { // Ensure we are in the "requesting keys" stage const currentState = self.state.get(StateKeys.bootstrapState); if (currentState?.stage !== BootstrapStage.RequestingKeys) return; // Remember the key currentState.keys.set(receivedCC.grantedKey, receivedCC.networkKey); // And temporarily use it for verification self.encodingContext.setSecurityClass(controller.ownNodeId, receivedCC.grantedKey, true); await sm2Node.setKey(receivedCC.grantedKey, receivedCC.networkKey); // Verify the key self.state.set(StateKeys.bootstrapState, { ...currentState, stage: BootstrapStage.VerifyingKeys, currentKey: receivedCC.grantedKey, }); await requestNonce(controller, self); return verifyKey(self, receivedCC.grantedKey); } }, }; async function requestNonce(controller, self) { const nonceGet = new Security2CCNonceGet({ nodeId: controller.ownNodeId, }); await self.sendToController(createMockZWaveRequestFrame(nonceGet, { ackRequested: false, })); const nonceReport = await self.expectControllerFrame((resp) => resp.type === MockZWaveFrameType.Request && resp.payload instanceof Security2CCNonceReport && resp.payload.SOS, { timeout: 1000 }); return nonceReport.payload.receiverEI; } // Returns the mock node action for requesting a network key function requestKey(self, secClass) { let networkKeyGet = new Security2CCNetworkKeyGet({ nodeId: self.controller.ownNodeId, requestedKey: secClass, }); networkKeyGet = Security2CC.encapsulate(networkKeyGet, self.id, self.securityManagers); return { action: "sendCC", cc: networkKeyGet }; } // Returns the mock node action for verifying a network key function verifyKey(self, secClass) { let networkKeyVerify = new Security2CCNetworkKeyVerify({ nodeId: self.controller.ownNodeId, }); networkKeyVerify = Security2CC.encapsulate(networkKeyVerify, self.id, self.securityManagers, { securityClass: secClass }); return { action: "sendCC", cc: networkKeyVerify }; } const handleS2TransferEnd = { async handleCC(controller, self, receivedCC) { const sm2Node = self.securityManagers.securityManager2; if (!sm2Node) return; if (receivedCC instanceof Security2CCTransferEnd && receivedCC.keyVerified && !receivedCC.keyRequestComplete && receivedCC.isEncapsulatedWith(CommandClasses["Security 2"])) { // Ensure we are in the "requesting keys" stage const currentState = self.state.get(StateKeys.bootstrapState); if (currentState?.stage !== BootstrapStage.RequestingKeys) return; const nextKey = currentState.grantedKeys[currentState.keys.size]; if (nextKey) { // Request the next key return requestKey(self, nextKey); } // We're done. The next command still needs to use the temporary // key though, so we cannot restore the security manager etc. // just yet. currentState.stage = BootstrapStage.Finalizing; let transferEnd = new Security2CCTransferEnd({ nodeId: controller.ownNodeId, keyVerified: false, keyRequestComplete: true, }); transferEnd = Security2CC.encapsulate(transferEnd, self.id, self.securityManagers); return { action: "sendCC", cc: transferEnd }; // The next communication attempt from the controller will // need a NonceGet, at which point we'll restore the security // manager etc. } }, }; // Create a new nonce when asked for const respondToS2NonceGet = { async handleCC(controller, self, receivedCC) { const sm2Node = self.securityManagers.securityManager2; if (!sm2Node) return; if (receivedCC instanceof Security2CCNonceGet) { const bootstrapState = self.state.get(StateKeys.bootstrapState); if (bootstrapState?.stage === BootstrapStage.VerifyingKeys) { // This is the NonceGet after sending the NetworkKeyVerify. // The next command will use the temporary key again, // so we need to reset the security manager for that. self.encodingContext.setSecurityClass(controller.ownNodeId, bootstrapState.currentKey, false); sm2Node.deleteNonce(controller.ownNodeId); // Reset the state back to requesting keys self.state.set(StateKeys.bootstrapState, { stage: BootstrapStage.RequestingKeys, grantedKeys: bootstrapState.grantedKeys, keys: bootstrapState.keys, }); } else if (bootstrapState?.stage === BootstrapStage.Finalizing) { // Bootstrapping is now actually done. Restore all keys // and security manager settings, so the communication uses // the correct security classes from now on. for (const [secClass, key] of bootstrapState.keys) { self.encodingContext.setSecurityClass(controller.ownNodeId, secClass, true); self.encodingContext.setSecurityClass(self.id, secClass, true); await sm2Node.setKey(secClass, key); sm2Node.tempKeys.delete(controller.ownNodeId); sm2Node.deleteNonce(controller.ownNodeId); } self.state.delete(StateKeys.bootstrapState); } const nonce = await sm2Node.generateNonce(controller.ownNodeId); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, SOS: true, MOS: false, receiverEI: nonce, }); return { action: "sendCC", cc }; } }, }; // Handle decode errors const handleS2DecodeError = { async handleCC(controller, self, receivedCC) { const sm2Node = self.securityManagers.securityManager2; if (!sm2Node) return; if (receivedCC instanceof InvalidCC) { if (receivedCC.reason === ZWaveErrorCodes.Security2CC_CannotDecode || receivedCC.reason === ZWaveErrorCodes.Security2CC_NoSPAN) { const nonce = await sm2Node.generateNonce(controller.ownNodeId); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, SOS: true, MOS: false, receiverEI: nonce, }); return { action: "sendCC", cc }; } } }, }; const respondToS2CommandsSupportedGet = { handleCC(controller, self, receivedCC) { if (receivedCC instanceof Security2CCCommandsSupportedGet) { const encapCC = receivedCC.getEncapsulatingCC(CommandClasses["Security 2"], Security2Command.MessageEncapsulation); if (!encapCC) return; const isHighestGranted = encapCC.securityClass === self.encodingContext.getHighestSecurityClass(self.id); const cc = Security2CC.encapsulate(new Security2CCCommandsSupportedReport({ nodeId: controller.ownNodeId, supportedCCs: isHighestGranted ? [...self.implementedCCs.entries()] .filter(([ccId, info]) => info.secure && ccId !== CommandClasses["Security 2"]) .map(([ccId]) => ccId) : [], }), self.id, self.securityManagers); return { action: "sendCC", cc }; } }, }; // Parse and unwrap Security2 CC commands. const encapsulateS2CC = { async transformIncomingCC(controller, self, receivedCC) { if (receivedCC instanceof Security2CCMessageEncapsulation && receivedCC.encapsulated) { receivedCC.encapsulated?.toggleEncapsulationFlag(EncapsulationFlags.Security, true); return receivedCC.encapsulated; } return receivedCC; }, async transformResponse(controller, self, receivedCC, response) { // Ensure that responses to S2-encapsulated CCs are also S2-encapsulated if (response.action === "sendCC" && receivedCC instanceof CommandClass && receivedCC.isEncapsulatedWith(CommandClasses["Security 2"]) && !(response.cc instanceof Security2CCMessageEncapsulation)) { // Encapsulate the response const encapsulated = Security2CC.encapsulate(response.cc, self.id, self.securityManagers); response.cc = encapsulated; } return response; }, }; export const Security2CCHooks = [ encapsulateS2CC, ]; export const Security2CCBehaviors = [ // Key exchange respondToS2KEXGet, respondToS2KEXSet, handleS2PublicKeyReport, respondToS2KEXReport, handleS2NetworkKeyReport, handleS2TransferEnd, // Normal operation respondToS2NonceGet, handleS2DecodeError, respondToS2CommandsSupportedGet, ]; //# sourceMappingURL=Security2.js.map