diff --git a/lnbits/extensions/watchonly/static/components/serial-signer/serial-signer.js b/lnbits/extensions/watchonly/static/components/serial-signer/serial-signer.js index f756eaaa3..b5cd6b577 100644 --- a/lnbits/extensions/watchonly/static/components/serial-signer/serial-signer.js +++ b/lnbits/extensions/watchonly/static/components/serial-signer/serial-signer.js @@ -14,6 +14,8 @@ async function serialSigner(path) { reader: null, receivedData: '', config: {}, + decryptionKey: null, + dheKey: null, // todo: store in secure local storage hww: { password: null, @@ -222,6 +224,9 @@ async function serialSigner(path) { case COMMAND_XPUB: this.handleXpubResponse(commandData) break + case COMMAND_DH_EXCHANGE: + this.handleDhExchangeResponse(commandData) + break default: console.log('### console', value) } @@ -234,7 +239,7 @@ async function serialSigner(path) { hwwShowPasswordDialog: async function () { try { this.hww.showPasswordDialog = true - await this.writer.write(COMMAND_PASSWORD + '\n') + await this.sendCommandSecure(COMMAND_PASSWORD) } catch (error) { this.$q.notify({ type: 'warning', @@ -247,7 +252,7 @@ async function serialSigner(path) { hwwShowWipeDialog: async function () { try { this.hww.showWipeDialog = true - await this.writer.write(COMMAND_WIPE + '\n') + await this.sendCommandSecure(COMMAND_WIPE) } catch (error) { this.$q.notify({ type: 'warning', @@ -260,7 +265,7 @@ async function serialSigner(path) { hwwShowRestoreDialog: async function () { try { this.hww.showRestoreDialog = true - await this.writer.write(COMMAND_WIPE + '\n') + await this.sendCommandSecure(COMMAND_WIPE) } catch (error) { this.$q.notify({ type: 'warning', @@ -275,11 +280,11 @@ async function serialSigner(path) { if (this.hww.confirm.outputIndex >= this.tx.outputs.length) { this.hww.confirm.showFee = true } - await this.writer.write(COMMAND_CONFIRM_NEXT + '\n') + await this.sendCommandSecure(COMMAND_CONFIRM_NEXT) }, cancelOperation: async function () { try { - await this.writer.write(COMMAND_CANCEL + '\n') + await this.sendCommandSecure(COMMAND_CANCEL) } catch (error) { this.$q.notify({ type: 'warning', @@ -297,9 +302,7 @@ async function serialSigner(path) { }, hwwLogin: async function () { try { - await this.writer.write( - COMMAND_PASSWORD + ' ' + this.hww.password + '\n' - ) + await this.sendCommandSecure(COMMAND_PASSWORD, [this.hww.password]) } catch (error) { this.$q.notify({ type: 'warning', @@ -335,7 +338,7 @@ async function serialSigner(path) { }, hwwLogout: async function () { try { - await this.writer.write(COMMAND_PASSWORD_CLEAR + '\n') + await this.sendCommandSecure(COMMAND_PASSWORD_CLEAR) } catch (error) { this.$q.notify({ type: 'warning', @@ -359,9 +362,7 @@ async function serialSigner(path) { try { this.tx = tx this.hww.sendingPsbt = true - await this.writer.write( - COMMAND_SEND_PSBT + ' ' + this.network + ' ' + psbtBase64 + '\n' - ) + await this.sendCommandSecure(COMMAND_SEND_PSBT, [this.network, psbtBase64]) this.$q.notify({ type: 'positive', message: 'Data sent to serial port device!', @@ -411,7 +412,7 @@ async function serialSigner(path) { try { this.hww.showConfirmationDialog = false this.hww.signingPsbt = true - await this.writer.write(COMMAND_SIGN_PSBT + '\n') + await this.sendCommandSecure(COMMAND_SIGN_PSBT) } catch (error) { this.$q.notify({ type: 'warning', @@ -442,47 +443,55 @@ async function serialSigner(path) { }) }, hwwHelp: async function () { - const sharedSecret = - 'f96c85875055a5586688fea4cf7c4a2bd9541ffcf34f9d663d97e0cf2f6af4af' - const sharedSecretBytes = hexToBytes(sharedSecret) - console.log('### sharedSecret', sharedSecret) - const key = await window.crypto.subtle.importKey( - 'raw', - sharedSecretBytes, - { - name: 'AES-CBC', - length: 256 - }, - true, - ['encrypt', 'decrypt'] - ) + // const sharedSecret = + // 'f96c85875055a5586688fea4cf7c4a2bd9541ffcf34f9d663d97e0cf2f6af4af' + // const sharedSecretBytes = hexToBytes(sharedSecret) + // // console.log('### sharedSecret', sharedSecret) + // const key = await window.crypto.subtle.importKey( + // 'raw', + // sharedSecretBytes, + // { + // name: 'AES-CBC', + // length: 256 + // }, + // true, + // ['encrypt', 'decrypt'] + // ) // d2b9e5e3ff8945236455424e9e25590b8264f13c7484862cca4f5b7b8bf8f1686d218b4f1aacdc27a1df71fa4b530adfd6c8cae6bd926d3f8be8ff55ee4358d1a32569e9f5263ffae7d0eaf413788498 // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx // 6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710 // 8f13a7763f021d7701f4100631f6c3d80576fcd0e3718b2594ceb7b910ceed29a334d1019dd6f0ffdba5b6be8c11637d6124d7adbd29c88af13800cb1f980f7d - const message = - 'TextMustBe16ByteTextMustBe16ByteTextMustBe16ByteTextMustBe16Byte' - const encoded = asciiToUint8Array(message) - const encrypted = await encryptMessage(key, encoded) - const encryptedHex = bytesToHex(encrypted) - console.log('### encrypted hex: ', encryptedHex) + // const message = + // 'TextMustBe16ByteTextMustBe16ByteTextMustBe16ByteTextMustBe16Byte' + // const encoded = asciiToUint8Array(message) + // const encrypted = await encryptMessage(key, encoded) + // const encryptedHex = bytesToHex(encrypted) + // console.log('### encrypted hex: ', encryptedHex) - const encryptedHex2 = await encryptMessage2(sharedSecretBytes, message) - console.log('### encryptedHex2', encryptedHex2) + // const encryptedHex2 = await encryptMessage2(sharedSecretBytes, message) + // console.log('### encryptedHex2', encryptedHex2) - const decrypted = await decryptMessage(key, encrypted) - console.log( - '### decrypted hex: ', - bytesToHex(new Uint8Array(decrypted)) - ) - console.log( - '### decrypted ascii: ', - new TextDecoder().decode(new Uint8Array(decrypted)) - ) + // const decrypted = await decryptMessage(key, encrypted) + // console.log( + // '### decrypted hex: ', + // bytesToHex(new Uint8Array(decrypted)) + // ) + // console.log( + // '### decrypted ascii: ', + // new TextDecoder().decode(new Uint8Array(decrypted)) + // ) try { - await this.writer.write(COMMAND_HELP + ' ' + encryptedHex + '\n') + this.decryptionKey = nobleSecp256k1.utils.randomPrivateKey() + const publicKey = nobleSecp256k1.Point.fromPrivateKey( + this.decryptionKey + ) + const publicKeyHex = publicKey.toHex().slice(2) + console.log('### publicKeyHex:', publicKeyHex) + await this.writer.write( + COMMAND_DH_EXCHANGE + ' ' + publicKeyHex + '\n' + ) this.$q.notify({ type: 'positive', message: 'Check display or console for details!', @@ -500,7 +509,7 @@ async function serialSigner(path) { hwwWipe: async function () { try { this.hww.showWipeDialog = false - await this.writer.write(COMMAND_WIPE + ' ' + this.hww.password + '\n') + await this.sendCommandSecure(COMMAND_WIPE, [this.hww.password]) } catch (error) { this.$q.notify({ type: 'warning', @@ -537,9 +546,8 @@ async function serialSigner(path) { '### hwwXpub', COMMAND_XPUB + ' ' + this.network + ' ' + path ) - await this.writer.write( - COMMAND_XPUB + ' ' + this.network + ' ' + path + '\n' - ) + + await this.sendCommandSecure(COMMAND_XPUB, [this.network, path]) } catch (error) { this.$q.notify({ type: 'warning', @@ -565,13 +573,46 @@ async function serialSigner(path) { const fingerprint = args[2].trim() this.xpubResolve({xpub, fingerprint}) }, + handleDhExchangeResponse: async function (res = '') { + console.log('### handleDhExchangeResponse', res) + const [pubKeyHex, fingerprint] = res.trim().split(' ') + if (!pubKeyHex) { + this.$q.notify({ + type: 'warning', + message: 'Failed to exchange DH secret!', + caption: `${res}`, + timeout: 10000 + }) + + return + } + const hwwPublicKey = nobleSecp256k1.Point.fromHex('04' + pubKeyHex) + + const sharedSecret = nobleSecp256k1.getSharedSecret( + this.decryptionKey, + hwwPublicKey + ).slice(1, 33) + console.log( + '### sharedSecret', + nobleSecp256k1.utils.bytesToHex(sharedSecret) + ) + this.dheKey = await window.crypto.subtle.importKey( + 'raw', + sharedSecret, + { + name: 'AES-CBC', + length: 256 + }, + true, + ['encrypt', 'decrypt'] + ) + }, hwwShowSeed: async function () { try { this.hww.showSeedDialog = true this.hww.seedWordPosition = 1 - await this.writer.write( - COMMAND_SEED + ' ' + this.hww.seedWordPosition + '\n' - ) + + await this.sendCommandSecure(COMMAND_SEED, [this.hww.seedWordPosition]) } catch (error) { this.$q.notify({ type: 'warning', @@ -583,16 +624,11 @@ async function serialSigner(path) { }, showNextSeedWord: async function () { this.hww.seedWordPosition++ - await this.writer.write( - COMMAND_SEED + ' ' + this.hww.seedWordPosition + '\n' - ) + await this.sendCommandSecure(COMMAND_SEED, [this.hww.seedWordPosition]) }, showPrevSeedWord: async function () { this.hww.seedWordPosition = Math.max(1, this.hww.seedWordPosition - 1) - console.log('### this.hww.seedWordPosition', this.hww.seedWordPosition) - await this.writer.write( - COMMAND_SEED + ' ' + this.hww.seedWordPosition + '\n' - ) + await this.sendCommandSecure(COMMAND_SEED, [this.hww.seedWordPosition]) }, handleShowSeedResponse: function (res = '') { const args = res.trim().split(' ') @@ -608,12 +644,8 @@ async function serialSigner(path) { }, hwwRestore: async function () { try { - await this.writer.write( - COMMAND_RESTORE + ' ' + this.hww.mnemonic + '\n' - ) - await this.writer.write( - COMMAND_PASSWORD + ' ' + this.hww.password + '\n' - ) + await this.sendCommandSecure(COMMAND_RESTORE, [this.hww.mnemonic]) + await this.sendCommandSecure(COMMAND_PASSWORD, [this.hww.password]) } catch (error) { this.$q.notify({ type: 'warning', @@ -633,8 +665,22 @@ async function serialSigner(path) { updateSignedPsbt: async function (value) { this.$emit('signed:psbt', value) + }, + + sendCommandSecure: async function (command, attrs = []) { + const message = [command].concat(attrs).join(' ') + + const encodedMessage = asciiToUint8Array(message.length + ' ' + message) + const encrypted = await encryptMessage(this.dheKey, encodedMessage) + + const encryptedHex = nobleSecp256k1.utils.bytesToHex(encrypted) + console.log('### encrypted hex: ', encryptedHex) + + await this.writer.write(encryptedHex + '\n') } }, - created: async function () {} + created: async function () { + console.log('### nobleSecp256k1.utils', nobleSecp256k1.utils) + } }) } diff --git a/lnbits/extensions/watchonly/static/components/wallet-config/wallet-config.js b/lnbits/extensions/watchonly/static/components/wallet-config/wallet-config.js index 5ce63c796..1f6789370 100644 --- a/lnbits/extensions/watchonly/static/components/wallet-config/wallet-config.js +++ b/lnbits/extensions/watchonly/static/components/wallet-config/wallet-config.js @@ -75,59 +75,54 @@ async function walletConfig(path) { ) }, old: async function () { - const alicesKeyPair = await window.crypto.subtle.generateKey( - { - name: 'ECDH', - namedCurve: 'P-256' - }, - true, - ['deriveKey'] - ) - const bobsKeyPair = await window.crypto.subtle.generateKey( - { - name: 'ECDH', - namedCurve: 'P-256' - }, - true, - ['deriveKey'] - ) - console.log('### alicesKeyPair', alicesKeyPair) - console.log('### alicesKeyPair.privateKey', alicesKeyPair.privateKey) - console.log('### alicesKeyPair.publicKey', alicesKeyPair.publicKey) - const alicesPriveKey = await window.crypto.subtle.exportKey( - 'jwk', - alicesKeyPair.privateKey - ) - console.log('### alicesPriveKey', alicesPriveKey) - - const alicesPriveKeyBase64 = toStdBase64(alicesPriveKey.d) - console.log( - '### alicesPriveKeyBase64', - alicesPriveKeyBase64, - base64ToHex(alicesPriveKeyBase64) - ) - - const bobPublicKey = await window.crypto.subtle.exportKey( - 'raw', - bobsKeyPair.publicKey - ) - console.log('### bobPublicKey hex', buf2hex(bobPublicKey)) - + // const alicesKeyPair = await window.crypto.subtle.generateKey( + // { + // name: 'ECDH', + // namedCurve: 'P-256' + // }, + // true, + // ['deriveKey'] + // ) + // const bobsKeyPair = await window.crypto.subtle.generateKey( + // { + // name: 'ECDH', + // namedCurve: 'P-256' + // }, + // true, + // ['deriveKey'] + // ) + // console.log('### alicesKeyPair', alicesKeyPair) + // console.log('### alicesKeyPair.privateKey', alicesKeyPair.privateKey) + // console.log('### alicesKeyPair.publicKey', alicesKeyPair.publicKey) + // const alicesPriveKey = await window.crypto.subtle.exportKey( + // 'jwk', + // alicesKeyPair.privateKey + // ) + // console.log('### alicesPriveKey', alicesPriveKey) + // const alicesPriveKeyBase64 = toStdBase64(alicesPriveKey.d) + // console.log( + // '### alicesPriveKeyBase64', + // alicesPriveKeyBase64, + // base64ToHex(alicesPriveKeyBase64) + // ) + // const bobPublicKey = await window.crypto.subtle.exportKey( + // 'raw', + // bobsKeyPair.publicKey + // ) + // console.log('### bobPublicKey hex', buf2hex(bobPublicKey)) // const sharedSecret01 = await this.deriveSecretKey(alicesKeyPair.privateKey, bobsKeyPair.publicKey) // console.log('### sharedSecret01', sharedSecret01) // const sharedSecret01Raw = await window.crypto.subtle.exportKey('jwk', sharedSecret01) // console.log('### sharedSecret01Raw', sharedSecret01Raw) - // const sharedSecret02 = await this.deriveSecretKey(bobsKeyPair.privateKey, alicesKeyPair.publicKey) // console.log('### sharedSecret02', sharedSecret02) // const sharedSecret02Raw = await window.crypto.subtle.exportKey('jwk', sharedSecret02) // console.log('### sharedSecret02Raw', sharedSecret02Raw) - - const sharedSecret = nobleSecp256k1.getSharedSecret( - alicesPriveKey, - buf2hex(bobPublicKey) - ) - console.log('###', getSharedSecret) + // const sharedSecret = nobleSecp256k1.getSharedSecret( + // alicesPriveKey, + // buf2hex(bobPublicKey) + // ) + // console.log('###', getSharedSecret) }, importSecretKey: async function (rawKey) { return window.crypto.subtle.importKey('raw', rawKey, 'AES-GCM', true, [ @@ -142,45 +137,45 @@ async function walletConfig(path) { // ### in: 6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710 // ### in2: 073059e23605fe508919d892501974905f8b5e13728db63b1b7b9326951abbe4f722c104bc82a4bcf3f1e15ede8ab7d5eb00be8c46271e1f65867f984e9cb5f1 - const alicesPrivateKey = - '359A8CA1418C49DD26DC7D92C789AC33347F64C6B7789C666098805AF3CC60E5' + // const alicesPrivateKey = + // '359A8CA1418C49DD26DC7D92C789AC33347F64C6B7789C666098805AF3CC60E5' - const bobsPrivateKey = - 'AB52F1F981F639BD83F884703BC690B10DB709FF48806680A0D3FBC6475E6093' + // const bobsPrivateKey = + // 'AB52F1F981F639BD83F884703BC690B10DB709FF48806680A0D3FBC6475E6093' - const alicesPublicKey = nobleSecp256k1.Point.fromPrivateKey( - alicesPrivateKey - ) - console.log('### alicesPublicKey', alicesPublicKey.toHex()) + // const alicesPublicKey = nobleSecp256k1.Point.fromPrivateKey( + // alicesPrivateKey + // ) + // console.log('### alicesPublicKey', alicesPublicKey.toHex()) - const bobsPublicKey = nobleSecp256k1.Point.fromPrivateKey(bobsPrivateKey) - console.log('### bobsPublicKey', bobsPublicKey.toHex()) + // const bobsPublicKey = nobleSecp256k1.Point.fromPrivateKey(bobsPrivateKey) + // console.log('### bobsPublicKey', bobsPublicKey.toHex()) - const sharedSecret = nobleSecp256k1.getSharedSecret( - alicesPrivateKey, - bobsPublicKey - ) + // const sharedSecret = nobleSecp256k1.getSharedSecret( + // alicesPrivateKey, + // bobsPublicKey + // ) - console.log('### sharedSecret naked', sharedSecret) + // console.log('### sharedSecret naked', sharedSecret) - console.log( - '### sharedSecret a', - nobleSecp256k1.utils.bytesToHex(sharedSecret) - ) - console.log( - '### sharedSecret b', - nobleSecp256k1.Point.fromHex(sharedSecret) - ) - console.log( - '### sharedSecret b', - nobleSecp256k1.Point.fromHex(sharedSecret).toHex(true) - ) + // console.log( + // '### sharedSecret a', + // nobleSecp256k1.utils.bytesToHex(sharedSecret) + // ) + // console.log( + // '### sharedSecret b', + // nobleSecp256k1.Point.fromHex(sharedSecret) + // ) + // console.log( + // '### sharedSecret b', + // nobleSecp256k1.Point.fromHex(sharedSecret).toHex(true) + // ) - const alicesPrivateKeyBytes = nobleSecp256k1.utils.hexToBytes( - alicesPrivateKey - ) - const x = await this.importSecretKey(alicesPrivateKeyBytes) - console.log('### x', x) + // const alicesPrivateKeyBytes = nobleSecp256k1.utils.hexToBytes( + // alicesPrivateKey + // ) + // const x = await this.importSecretKey(alicesPrivateKeyBytes) + // console.log('### x', x) } }) } diff --git a/lnbits/extensions/watchonly/static/js/utils.js b/lnbits/extensions/watchonly/static/js/utils.js index 3ac59ce8e..a83be7773 100644 --- a/lnbits/extensions/watchonly/static/js/utils.js +++ b/lnbits/extensions/watchonly/static/js/utils.js @@ -10,6 +10,7 @@ const COMMAND_RESTORE = '/restore' const COMMAND_CONFIRM_NEXT = '/confirm-next' const COMMAND_CANCEL = '/cancel' const COMMAND_XPUB = '/xpub' +const COMMAND_DH_EXCHANGE = '/dh-exchange' const DEFAULT_RECEIVE_GAP_LIMIT = 20 @@ -184,20 +185,7 @@ function findAccountPathIssues(path = '') { /////////////// let ivHex = '000102030405060708090a0b0c0d0e0f' -function hexToBytes(hex) { - const bytes = new Uint8Array(hex.length / 2) - for (c = 0; c < hex.length; c += 2) - bytes[c / 2] = parseInt(hex.substr(c, 2), 16) - return bytes -} -function bytesToHex(bytes) { - for (var hex = [], i = 0; i < bytes.length; i++) { - var current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i] - hex.push((current >>> 4).toString(16)) - hex.push((current & 0xf).toString(16)) - } - return hex.join('') -} + function asciiToUint8Array(str) { var chars = [] for (var i = 0; i < str.length; ++i) { @@ -205,37 +193,6 @@ function asciiToUint8Array(str) { } return new Uint8Array(chars) } -async function encryptMessage2(key, text) { - console.log('### key', bytesToHex(key)) - iv = new Uint8Array([ - 0x00, - 0x01, - 0x02, - 0x03, - 0x04, - 0x05, - 0x06, - 0x07, - 0x08, - 0x09, - 0x0a, - 0x0b, - 0x0c, - 0x0d, - 0x0e, - 0x0f - ]) - var textBytes = aesjs.utils.utf8.toBytes(text) - console.log('### textBytes2', bytesToHex(textBytes)) - console.log('### iv', bytesToHex(iv)) - - var aesCbc = new aesjs.ModeOfOperation.cbc(key, iv) - var encryptedBytes = aesCbc.encrypt(textBytes) - - // To print or store the binary data, you may convert it to hex - var encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes) - return encryptedHex -} async function encryptMessage(key, message) { // The iv must never be reused with a given key. @@ -302,38 +259,3 @@ async function decryptMessage(key, ciphertext) { return decrypted } - -// TODO: weak: must re-check -// from https://stackoverflow.com/a/51838635/9014097 -function toStdBase64(input) { - input = input.replace(/-/g, '+').replace(/_/g, '/') - var pad = input.length % 4 - if (pad) { - input += new Array(5 - pad).join('=') - } - return input -} - -// TODO: weak: must re-check -function base64ToHex(str) { - const raw = atob(str) - let result = '' - for (let i = 0; i < raw.length; i++) { - const hex = raw.charCodeAt(i).toString(16) - result += hex.length === 2 ? hex : '0' + hex - } - return result.toUpperCase() -} - -function buf2hex(buffer) { - // buffer is an ArrayBuffer - return [...new Uint8Array(buffer)] - .map(x => x.toString(16).padStart(2, '0')) - .join('') -} - -// Alice private key -// 359A8CA1418C49DD26DC7D92C789AC33347F64C6B7789C666098805AF3CC60E5 - -// Bob private key -// AB52F1F981F639BD83F884703BC690B10DB709FF48806680A0D3FBC6475E6093