UNPKG

obniz

Version:

obniz sdk for javascript

438 lines (437 loc) 18.9 kB
"use strict"; /** * @packageDocumentation * * @ignore */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Smp = void 0; const eventemitter3_1 = __importDefault(require("eventemitter3")); const ObnizError_1 = require("../../../../../ObnizError"); const bleHelper_1 = __importDefault(require("../../bleHelper")); const crypto_1 = __importDefault(require("../common/crypto")); const smp_1 = require("../common/smp"); const serial_executor_1 = require("@9wick/serial-executor"); /** * @ignore */ class Smp extends eventemitter3_1.default { constructor(aclStream, localAddressType, localAddress, remoteAddressType, remoteAddress) { super(); this._preq = null; // pairing Request buffer this._pres = null; // pairing Response buffer this._pairingFeature = null; // conbine (pairing Request & pairing Response) this._tk = null; this._r = null; this._rand = null; this._ediv = null; this._pcnf = null; this._stk = null; this._ltk = null; this._options = undefined; this._smpCommon = new smp_1.SmpCommon(); this._serialExecutor = (0, serial_executor_1.createSerialExecutor)(); this._pairingPromise = null; this.debugHandler = (...param) => { // do nothing. }; this._aclStream = aclStream; this._iat = Buffer.from([localAddressType === 'random' ? 0x01 : 0x00]); this._ia = bleHelper_1.default.hex2reversedBuffer(localAddress, ':'); this._rat = Buffer.from([remoteAddressType === 'random' ? 0x01 : 0x00]); this._ra = bleHelper_1.default.hex2reversedBuffer(remoteAddress, ':'); this.onAclStreamDataBinded = this.onAclStreamData.bind(this); this.onAclStreamEndBinded = this.onAclStreamEnd.bind(this); this._aclStream.on('data', this.onAclStreamDataBinded); this._aclStream.on('end', this.onAclStreamEndBinded); } async pairingWithKeyWait(key) { this.debug(`Pairing using keys ${key}`); this.setKeys(key); if (!this._ltk || !this._rand || !this._ediv) { throw new Error('invalid keys'); } // console.log(this._smpCommon.parsePairingReqRsp(this._pres!)); const encResult = await this._aclStream.onSmpLtkWait(this._ltk, this._rand, this._ediv); return encResult; } setPairingOption(options) { this._options = options; } async pairingWait(options) { // allow only once for connection if (this._pairingPromise) { return await this._pairingPromise; } this._pairingPromise = this._serialExecutor.execute(async () => { return await this.pairingSingleQueueWait(options); }); return await this._pairingPromise; } async pairingSingleQueueWait(options) { this._options = Object.assign(Object.assign({}, this._options), options); // if already paired if (this.hasKeys()) { if (this._options && this._options.onPairedCallback) { this._options.onPairedCallback(this.getKeys()); } return; } if (this._options && this._options.keys) { const result = await this.pairingWithKeyWait(this._options.keys); if (this._options && this._options.onPairedCallback) { this._options.onPairedCallback(this.getKeys()); } return; } // phase 1 : Pairing Feature Exchange this.debug(`Going to Pairing`); await this.sendPairingRequestWait(); this.debug(`Waiting Pairing Response`); const pairingResponse = await this._readWait(smp_1.SMP.PAIRING_RESPONSE); this.debug(`Receive Pairing Response ${pairingResponse.toString('hex')}`); this._pres = pairingResponse; const parsedPairingRequest = this._smpCommon.parsePairingReqRsp(this._preq); const parsedPairingResponse = this._smpCommon.parsePairingReqRsp(this._pres); this._pairingFeature = this._smpCommon.combinePairingParam(parsedPairingRequest, parsedPairingResponse); if (this.isSecureConnectionMode()) { if (!this._pairingFeature.sc) { throw new ObnizError_1.ObnizBleUnSupportedPeripheralError('secure connection'); } // phase2 : (after receive PAIRING_RESPONSE) await this.handlePairingResponseSecureConnectionWait(); await new Promise((resolve) => setTimeout(resolve, 1000)); } else { // phase2 : (after receive PAIRING_RESPONSE) await this.handlePairingResponseLegacyPairingWait(); this.debug(`Waiting Pairing Confirm`); const confirm = await this._readWait(smp_1.SMP.PAIRING_CONFIRM, 60 * 1000); // 60sec timeout this.handlePairingConfirm(confirm); // phase3 : Transport Specific Key Distribution this.debug(`Waiting Pairing Random`); const random = await this._readWait(smp_1.SMP.PAIRING_RANDOM); const encResultPromise = this.handlePairingRandomWait(random); this.debug(`Got Pairing Encryption Result`); const encInfoPromise = this._readWait(smp_1.SMP.ENCRYPT_INFO); const masterIdentPromise = this._readWait(smp_1.SMP.MASTER_IDENT); await Promise.all([encResultPromise, encInfoPromise, masterIdentPromise]); const encResult = await encResultPromise; const encInfo = await encInfoPromise; const masterIdent = await masterIdentPromise; this.handleEncryptInfo(encInfo); this.handleMasterIdent(masterIdent); if (encResult === 0) { throw new Error('Encrypt failed'); } } if (this._options && this._options.onPairedCallback) { this._options.onPairedCallback(this.getKeys()); } } onAclStreamData(cid, data) { if (cid !== smp_1.SMP.CID) { return; } const code = data.readUInt8(0); if (smp_1.SMP.PAIRING_FAILED === code) { this.handlePairingFailed(data); } else if (smp_1.SMP.SMP_SECURITY_REQUEST === code) { this.handleSecurityRequest(data); } // console.warn("SMP: " + code); } onAclStreamEnd() { this._aclStream.removeListener('data', this.onAclStreamDataBinded); this._aclStream.removeListener('end', this.onAclStreamEndBinded); this.emit('end'); } async handlePairingResponseLegacyPairingWait() { if (this.isPasskeyMode()) { let passkeyNumber = 0; passkeyNumber = await this._options.passkeyCallback(); this.debug(`PassKey=${passkeyNumber}`); const passkey = new Array(16); for (let i = 0; i < 3; i++) { passkey[i] = (passkeyNumber >> (i * 8)) & 0xff; } this._tk = Buffer.from(passkey); } else { this._tk = Buffer.from('00000000000000000000000000000000', 'hex'); } this._r = crypto_1.default.r(); this.write(Buffer.concat([ Buffer.from([smp_1.SMP.PAIRING_CONFIRM]), crypto_1.default.c1(this._tk, this._r, this._pres, this._preq, this._iat, this._ia, this._rat, this._ra), ])); } async handlePairingResponseSecureConnectionWait() { var _a, _b, _c; const ecdh = crypto_1.default.createECDHKey(); const usePasskey = ((_a = this._pairingFeature) === null || _a === void 0 ? void 0 : _a.association) === 'PasskeyEntryRspInputs' || ((_b = this._pairingFeature) === null || _b === void 0 ? void 0 : _b.association) === 'PasskeyEntryBothInputs' || ((_c = this._pairingFeature) === null || _c === void 0 ? void 0 : _c.association) === 'PasskeyEntryInitInputs'; const remoteKeyPromise = this._readWait(smp_1.SMP.PAIRING_PUBLIC_KEY); const remoteConfirmForJustWorksPromise = usePasskey ? null : this._readWait(smp_1.SMP.PAIRING_CONFIRM); this.write(Buffer.concat([Buffer.from([smp_1.SMP.PAIRING_PUBLIC_KEY]), ecdh.x, ecdh.y])); const remoteKey = await remoteKeyPromise; const peerPublicKey = { x: remoteKey.slice(1, 33), y: remoteKey.slice(33, 65), }; this.debug('got remote public key'); let passkeyNumber = 0; if (usePasskey) { passkeyNumber = await this._options.passkeyCallback(); this.debug(`PassKey=${passkeyNumber}`); if (passkeyNumber < 0 || passkeyNumber > 999999) { throw new ObnizError_1.ObnizBleInvalidPasskeyError(passkeyNumber); } } const rspConfirmBuffers = []; const rspRandomBuffers = []; const initRandomValue = crypto_1.default.randomBytes(16); if (!usePasskey) { this.debug(`pairing confirm only once`); const buf = await remoteConfirmForJustWorksPromise; rspConfirmBuffers.push(buf.slice(1)); const remoteRamdomPromise = this._readWait(smp_1.SMP.PAIRING_RANDOM); this.write(Buffer.concat([Buffer.from([smp_1.SMP.PAIRING_RANDOM]), initRandomValue])); const buf2 = await remoteRamdomPromise; rspRandomBuffers.push(buf2.slice(1)); } else { for (let passkeyBitCounter = 0; passkeyBitCounter < 20; passkeyBitCounter++) { this.debug(`pairing confirm loop:${passkeyBitCounter}`); const remoteConfirmPromise = this._readWait(smp_1.SMP.PAIRING_CONFIRM); const initConfirmValue = crypto_1.default.f4(ecdh.x, peerPublicKey.x, initRandomValue, ((passkeyNumber >> passkeyBitCounter) & 1) | 0x80); this.write(Buffer.concat([Buffer.from([smp_1.SMP.PAIRING_CONFIRM]), initConfirmValue])); const buf = await remoteConfirmPromise; rspConfirmBuffers.push(buf.slice(1)); const remoteRamdomPromise = this._readWait(smp_1.SMP.PAIRING_RANDOM); this.write(Buffer.concat([Buffer.from([smp_1.SMP.PAIRING_RANDOM]), initRandomValue])); const buf2 = await remoteRamdomPromise; rspRandomBuffers.push(buf2.slice(1)); } } const res = crypto_1.default.generateLtkEaEb(ecdh.ecdh, peerPublicKey, this._ia, this._iat, this._ra, this._rat, initRandomValue, rspRandomBuffers[rspRandomBuffers.length - 1], passkeyNumber !== null && passkeyNumber !== void 0 ? passkeyNumber : 0, 0x10, // max key size this._preq ? this._preq.slice(1, 4) : Buffer.alloc(3), this._pres ? this._pres.slice(1, 4) : Buffer.alloc(3)); const remoteDhkeyPromise = this._readWait(smp_1.SMP.PAIRING_DHKEY_CHECK); this.debug(`send PAIRING_DHKEY_CHECK`); this.write(Buffer.concat([Buffer.from([smp_1.SMP.PAIRING_DHKEY_CHECK]), res.Ea])); const buf3 = await remoteDhkeyPromise; this.debug(`receive PAIRING_DHKEY_CHECK`); const Eb = buf3.slice(1); const irkPromise = this._readWait(smp_1.SMP.IDENTITY_INFORMATION); const bdAddrPromise = this._readWait(smp_1.SMP.IDENTITY_ADDRESS_INFORMATION); this.debug(`receive IDENTITY_INFORMATION IDENTITY_ADDRESS_INFORMATION`); this._ltk = res.ltk; this.emit('ltk', this._ltk); await this._aclStream.onSmpStkWait(this._ltk); const irkBuf = await irkPromise; const bdAddrBuf = await bdAddrPromise; // we dont have irk, so zero padding this.write(Buffer.concat([Buffer.from([smp_1.SMP.IDENTITY_INFORMATION]), Buffer.alloc(16)])); this.write(Buffer.concat([ Buffer.from([smp_1.SMP.IDENTITY_ADDRESS_INFORMATION]), this._iat, this._ia, ])); this._rand = Buffer.alloc(8); this._ediv = Buffer.alloc(2); return res.ltk; } handlePairingConfirm(data) { var _a; this._pcnf = data; this.write(Buffer.concat([ Buffer.from([smp_1.SMP.PAIRING_RANDOM]), (_a = this._r) !== null && _a !== void 0 ? _a : Buffer.alloc(0), ])); } async handlePairingRandomWait(data) { const r = data.slice(1); let encResult = null; const pcnf = Buffer.concat([ Buffer.from([smp_1.SMP.PAIRING_CONFIRM]), crypto_1.default.c1(this._tk, r, this._pres, this._preq, this._iat, this._ia, this._rat, this._ra), ]); if (this._pcnf && this._pcnf.toString('hex') === pcnf.toString('hex')) { if (this._stk !== null) { console.error('second stk'); } this._stk = crypto_1.default.s1(this._tk, r, this._r); // this.emit("stk", this._stk); encResult = await this._aclStream.onSmpStkWait(this._stk); } else { this.write(Buffer.from([smp_1.SMP.PAIRING_RANDOM, smp_1.SMP.PAIRING_CONFIRM])); this.emit('fail', 0); throw new Error('Encryption pcnf error'); } return encResult; } handlePairingFailed(data) { this.emit('fail', data.readUInt8(1)); } handleEncryptInfo(data) { this._ltk = data.slice(1); this.emit('ltk', this._ltk); } handleMasterIdent(data) { const ediv = data.slice(1, 3); const rand = data.slice(3); this._ediv = ediv; this._rand = rand; this.emit('masterIdent', ediv, rand); } write(data) { this._aclStream.write(smp_1.SMP.CID, data); } handleSecurityRequest(data) { this.pairingWait() .then(() => { // do nothing. }) .catch((e) => { if (this._options && this._options.onPairingFailed) { this._options.onPairingFailed(e); } else { e.cause = new Error('pairing failed when remote device request'); console.error(e); } }); } setKeys(keyStringBase64) { const keyString = Buffer.from(keyStringBase64, 'base64').toString('ascii'); this.debug(`restored keys ${keyString}`); const keys = JSON.parse(keyString); this._stk = keys.stk ? Buffer.from(keys.stk, 'hex') : null; this._preq = keys.preq ? Buffer.from(keys.preq, 'hex') : null; this._pres = keys.pres ? Buffer.from(keys.pres, 'hex') : null; this._tk = keys.tk ? Buffer.from(keys.tk, 'hex') : null; this._r = keys.r ? Buffer.from(keys.r, 'hex') : null; this._pcnf = keys.pcnf ? Buffer.from(keys.pcnf, 'hex') : null; this._ltk = keys.ltk ? Buffer.from(keys.ltk, 'hex') : null; this._ediv = keys.ediv ? Buffer.from(keys.ediv, 'hex') : null; this._rand = keys.rand ? Buffer.from(keys.rand, 'hex') : null; } hasKeys() { if (!this._ltk || !this._rand || !this._ediv) { return false; } return true; } getKeys() { const keys = { stk: this._stk ? this._stk.toString('hex') : null, preq: this._preq ? this._preq.toString('hex') : null, pres: this._pres ? this._pres.toString('hex') : null, tk: this._tk ? this._tk.toString('hex') : null, r: this._r ? this._r.toString('hex') : null, pcnf: this._pcnf ? this._pcnf.toString('hex') : null, ltk: this._ltk ? this._ltk.toString('hex') : null, ediv: this._ediv ? this._ediv.toString('hex') : null, rand: this._rand ? this._rand.toString('hex') : null, }; const jsonString = JSON.stringify(keys); const keyString = Buffer.from(jsonString, 'ascii').toString('base64'); return keyString; } _generateAuthenticationRequirementsFlags(params) { let result = 0x00; if (params.bonding === 'Bonding') { result |= 0x01; } if (params.mitm) { result |= 0x04; } if (params.secureConnection) { result |= 0x08; } if (params.keypress) { result |= 0x10; } if (params.ct2) { result |= 0x20; } return result; } async sendPairingRequestWait() { if (this.isPasskeyMode()) { this.debug(`pair capable passkey`); this._preq = Buffer.from([ smp_1.SMP.PAIRING_REQUEST, 0x02, 0x00, this._generateAuthenticationRequirementsFlags({ bonding: 'Bonding', mitm: true, ct2: false, keypress: false, secureConnection: this.isSecureConnectionMode(), }), 0x10, this.isSecureConnectionMode() ? 0x02 : 0x00, this.isSecureConnectionMode() ? 0x02 : 0x01, // Responder key distribution: EncKey peripheral -> central ]); } else { this.debug(`pair No Input and No Output`); this._preq = Buffer.from([ smp_1.SMP.PAIRING_REQUEST, 0x03, 0x00, this._generateAuthenticationRequirementsFlags({ bonding: 'Bonding', mitm: false, ct2: false, keypress: false, secureConnection: this.isSecureConnectionMode(), }), 0x10, this.isSecureConnectionMode() ? 0x02 : 0x00, this.isSecureConnectionMode() ? 0x02 : 0x01, // Responder key distribution: EncKey ]); } this.write(this._preq); } isPasskeyMode() { if (this._options && this._options.passkeyCallback) { return true; } return false; } isSecureConnectionMode() { if (this._options && this._options.secureConnection) { return true; } return false; } _readWait(flag, timeout) { return Promise.race([ this._aclStream.readWait(smp_1.SMP.CID, flag, timeout), this._pairingFailReject(), ]); } _pairingFailReject() { return new Promise((resolve, reject) => { const cause = new Error('stacktrace'); this.on('fail', (reason) => { reject(new ObnizError_1.ObnizBlePairingRejectByRemoteError(reason, { cause })); }); }); } debug(text) { // console.log(new Date(), `SMP: ${text}`); } } exports.Smp = Smp;