diff --git a/export/index.html b/export/index.html index b440819..3f30b65 100644 --- a/export/index.html +++ b/export/index.html @@ -17,7 +17,10 @@ display:inline-block; width: 8em; } - input[type=text] { + form { + text-align: left; + } + input[type=text], select { width: 40em; margin: 0.5em; font-family: 'Courier New', Courier, monospace; @@ -81,16 +84,27 @@

Export Key Material




-

Inject Export Bundle

+

Inject Key Export Bundle

The export bundle comes from the parent page and is composed of a public key and an encrypted payload. The payload is encrypted to this document's embedded key (stored in local storage and displayed above). The scheme relies on HPKE (RFC 9180).

- + +
+ + +
+ +

+

Inject Wallet Export Bundle

+

The export bundle comes from the parent page and is composed of a public key and an encrypted payload. The payload is encrypted to this document's embedded key (stored in local storage and displayed above). The scheme relies on HPKE (RFC 9180).

- +
@@ -268,12 +282,120 @@

Message log

} /** - * Returns a hex-encoded or base58-encoded private key from private key bytes. + * Encodes a buffer into a base58-encoded string. + * @param {Uint8Array} bytes The buffer to encode. + * @return {string} The base58-encoded string. + */ + function base58Encode(bytes) { + // See https://en.bitcoin.it/wiki/Base58Check_encoding + const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + let result = ''; + let digits = [0]; + for (let i = 0; i < bytes.length; i++) { + let carry = bytes[i]; + for (let j = 0; j < digits.length; ++j) { + carry += digits[j] << 8; + digits[j] = carry % 58; + carry = (carry / 58) | 0; + } + + while (carry > 0) { + digits.push(carry % 58); + carry = (carry / 58) | 0; + } + } + // Convert digits to a base58 string + for (let k = 0; k < digits.length; k++) { + result = alphabet[digits[k]] + result; + } + + // Add '1' for each leading 0 byte + for (let i = 0; bytes[i] === 0 && i < bytes.length - 1; i++) { + result = '1' + result; + } + return result; + } + + /** + * Decodes a base58-encoded string into a buffer + * This function throws an error when the string contains invalid characters. + * @param {string} s The base58-encoded string. + * @return {Uint8Array} The decoded buffer. + */ + function base58Decode(s) { + // See https://en.bitcoin.it/wiki/Base58Check_encoding + var alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + var decoded = BigInt(0); + var decodedBytes = []; + var leadingZeros = []; + for (var i = 0; i < s.length; i++) { + if (alphabet.indexOf(s[i]) === -1) { + throw new Error(`cannot base58-decode: ${s[i]} isn't a valid character`) + } + var carry = alphabet.indexOf(s[i]); + + // If the current base58 digit is 0, append a 0 byte. + // "i == leadingZeros.length" can only be true if we have not seen non-zero bytes so far. + // If we had seen a non-zero byte, carry wouldn't be 0, and i would be strictly more than `leadingZeros.length` + if (carry == 0 && i === leadingZeros.length) { + leadingZeros.push(0); + } + + var j = 0; + while (j < decodedBytes.length || carry > 0) { + var currentByte = decodedBytes[j]; + + // shift the current byte 58 units and add the carry amount + // (or just add the carry amount if this is a new byte -- undefined case) + if (currentByte === undefined) { + currentByte = carry + } else { + currentByte = currentByte * 58 + carry + } + + // find the new carry amount (1-byte shift of current byte value) + carry = currentByte >> 8; + // reset the current byte to the remainder (the carry amount will pass on the overflow) + decodedBytes[j] = currentByte % 256; + j++ + } + } + + var result = leadingZeros.concat(decodedBytes.reverse()); + return new Uint8Array(result); + } + + /** + * Returns a private key from private key bytes, represented in + * the encoding and format specified by `keyFormat`. Defaults to + * hex-encoding if `keyFormat` isn't passed. * @param {Uint8Array} privateKeyBytes + * @param {string} keyFormat Can be "HEXADECIMAL" or "SOLANA" + * @param {string} publicKey Hex-encoded public key, needed for Solana private keys */ - const parseKey = privateKeyBytes => { - const decoder = new TextDecoder("utf-8"); - return decoder.decode(privateKeyBytes); + const parseKey = (privateKeyBytes, keyFormat, publicKey) => { + switch (keyFormat) { + case "SOLANA": + if (!publicKey) { + throw new Error("public key must be specified for SOLANA key format"); + } + if (privateKeyBytes.length !== 32) { + throw new Error(`invalid private key length. Expected 32 bytes. Got ${privateKeyBytes.length()}.`); + } + const publicKeyBytes = uint8arrayFromHexString(publicKey); + if (publicKeyBytes.length !== 32) { + throw new Error(`invalid public key length. Expected 32 bytes. Got ${privateKeyBytes.length()}.`); + } + const concatenatedBytes = new Uint8Array(64); + concatenatedBytes.set(privateKeyBytes, 0); + concatenatedBytes.set(publicKeyBytes, 32); + return base58Encode(concatenatedBytes); + case "HEXADECIMAL": + return "0x" + uint8arrayToHexString(privateKeyBytes); + default: + console.warn(`invalid key format: ${keyFormat}. Defaulting to HEXADECIMAL.`); + return "0x" + uint8arrayToHexString(privateKeyBytes); + } } /** @@ -310,6 +432,8 @@

Message log

setEmbeddedKey, onResetEmbeddedKey, p256JWKPrivateToPublic, + base58Encode, + base58Decode, parseKey, parseWallet, sendMessageUp, @@ -342,9 +466,9 @@

Message log

// We do not want to arbitrarily receive messages from all origins. window.addEventListener("message", async function(event) { if (event.data && event.data["type"] == "INJECT_KEY_EXPORT_BUNDLE") { - TKHQ.logMessage(`⬇️ Received message ${event.data["type"]}: ${event.data["value"]}`); + TKHQ.logMessage(`⬇️ Received message ${event.data["type"]}: ${event.data["value"]}, ${event.data["keyFormat"]}, ${event.data["publicKey"]}`); try { - await onInjectKeyBundle(event.data["value"]) + await onInjectKeyBundle(event.data["value"], event.data["keyFormat"], event.data["publicKey"]) } catch (e) { TKHQ.sendMessageUp("ERROR", e.toString()); } @@ -377,6 +501,8 @@

Message log

window.postMessage({ "type": "INJECT_KEY_EXPORT_BUNDLE", "value": document.getElementById("key-export-bundle").value, + "keyFormat": document.getElementById("key-export-format").value, + "publicKey": document.getElementById("key-export-public-key").value, }) }, false); document.getElementById("inject-wallet").addEventListener("click", async e => { @@ -447,12 +573,12 @@

Message log

* Function triggered when INJECT_KEY_EXPORT_BUNDLE event is received. * @param {string} bundle */ - const onInjectKeyBundle = async bundle => { + const onInjectKeyBundle = async (bundle, keyFormat, publicKey) => { // Decrypt the export bundle const keyBytes = await decryptBundle(bundle); // Parse the decrypted key bytes - const key = TKHQ.parseKey(new Uint8Array(keyBytes)); + const key = TKHQ.parseKey(new Uint8Array(keyBytes), keyFormat, publicKey); // Display only the key displayKey(key); diff --git a/export/index.test.js b/export/index.test.js index ab7d7e0..f1e2b84 100644 --- a/export/index.test.js +++ b/export/index.test.js @@ -84,20 +84,28 @@ describe("TKHQ", () => { expect(key.key_ops).toContain("deriveBits"); }) + it("parses hex-encoded private key correctly by default", async () => { + const keyHex = "0x13eff5b3f9c63eab5d53cff5149f01606b69325496e0e98b53afa938d890cd2e"; + const parsedKey = TKHQ.parseKey(TKHQ.uint8arrayFromHexString(keyHex.slice(2))); + expect(parsedKey).toEqual(keyHex); + }) + it("parses hex-encoded private key correctly", async () => { - const keyHex = "13eff5b3f9c63eab5d53cff5149f01606b69325496e0e98b53afa938d890cd2e"; - const encoder = new TextEncoder("utf-8"); - const encodedKey = encoder.encode(keyHex); - const parsedKey = TKHQ.parseKey(encodedKey); + const keyHex = "0x13eff5b3f9c63eab5d53cff5149f01606b69325496e0e98b53afa938d890cd2e"; + const parsedKey = TKHQ.parseKey(TKHQ.uint8arrayFromHexString(keyHex.slice(2)), "HEXADECIMAL"); expect(parsedKey).toEqual(keyHex); }) - it("parses base58-encoded private key correctly", async () => { - const keybase58 = "5HueCGU8rMjxExZhSwp1xXQPBDsMaZwk74rZkDfDXvDVpi7L6vBZp2uhZLyStgM9xXdwvCLSrqQfJCVDqWsRU8T7"; - const encoder = new TextEncoder("utf-8"); - const encodedKey = encoder.encode(keybase58); - const parsedKey = TKHQ.parseKey(encodedKey); - expect(parsedKey).toEqual(keybase58); + it("parses solana private key correctly", async () => { + const keySol = "2P3qgS5A18gGmZJmYHNxYrDYPyfm6S3dJgs8tPW6ki6i2o4yx7K8r5N8CF7JpEtQiW8mx1kSktpgyDG1xuWNzfsM"; + const keySolBytes = TKHQ.base58Decode(keySol); + expect(keySolBytes.length).toEqual(64); + const keyPrivBytes = keySolBytes.subarray(0, 32); + const keyPubBytes = keySolBytes.subarray(32, 64); + const keyPubHex = TKHQ.uint8arrayToHexString(keyPubBytes); + + const parsedKey = TKHQ.parseKey(keyPrivBytes, "SOLANA", keyPubHex); + expect(parsedKey).toEqual(keySol); }) it("parses wallet with only mnemonic correctly", async () => { diff --git a/import/index.html b/import/index.html index 7928ada..5b38148 100644 --- a/import/index.html +++ b/import/index.html @@ -11,7 +11,6 @@ Turnkey Import @@ -93,6 +90,84 @@ .join(''); } + /** + * Decodes a base58-encoded string into a buffer + * This function throws an error when the string contains invalid characters. + * @param {string} s The base58-encoded string. + * @return {Uint8Array} The decoded buffer. + */ + function base58Decode(s) { + // See https://en.bitcoin.it/wiki/Base58Check_encoding + var alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + var decoded = BigInt(0); + var decodedBytes = []; + var leadingZeros = []; + for (var i = 0; i < s.length; i++) { + if (alphabet.indexOf(s[i]) === -1) { + throw new Error(`cannot base58-decode: ${s[i]} isn't a valid character`) + } + var carry = alphabet.indexOf(s[i]); + + // If the current base58 digit is 0, append a 0 byte. + // "i == leadingZeros.length" can only be true if we have not seen non-zero bytes so far. + // If we had seen a non-zero byte, carry wouldn't be 0, and i would be strictly more than `leadingZeros.length` + if (carry == 0 && i === leadingZeros.length) { + leadingZeros.push(0); + } + + var j = 0; + while (j < decodedBytes.length || carry > 0) { + var currentByte = decodedBytes[j]; + + // shift the current byte 58 units and add the carry amount + // (or just add the carry amount if this is a new byte -- undefined case) + if (currentByte === undefined) { + currentByte = carry + } else { + currentByte = currentByte * 58 + carry + } + + // find the new carry amount (1-byte shift of current byte value) + carry = currentByte >> 8; + // reset the current byte to the remainder (the carry amount will pass on the overflow) + decodedBytes[j] = currentByte % 256; + j++ + } + } + + var result = leadingZeros.concat(decodedBytes.reverse()); + return new Uint8Array(result); + } + + /** + * Returns private key bytes from a private key, represented in + * the encoding and format specified by `keyFormat`. Defaults to + * hex-encoding if `keyFormat` isn't passed. + * @param {string} privateKey + * @param {string} keyFormat Can be "HEXADECIMAL" or "SOLANA" + */ + const formatKey = (privateKey, keyFormat) => { + switch (keyFormat) { + case "SOLANA": + const decodedKeyBytes = base58Decode(privateKey); + if (decodedKeyBytes.length !== 64) { + throw new Error(`invalid key length. Expected 64 bytes. Got ${decodedKeyBytes.length()}.`); + } + return decodedKeyBytes.subarray(0, 32); + case "HEXADECIMAL": + if (privateKey.startsWith("0x")) { + return uint8arrayFromHexString(privateKey.slice(2)); + } + return uint8arrayFromHexString(privateKey); + default: + console.warn(`invalid key format: ${keyFormat}. Defaulting to HEXADECIMAL.`); + if (privateKey.startsWith("0x")) { + return uint8arrayFromHexString(privateKey.slice(2)); + } + return uint8arrayFromHexString(privateKey); + } + } + /** * Additional Associated Data (AAD) in the format dictated by the enclave_encrypt crate. */ @@ -123,6 +198,8 @@ sendMessageUp, uint8arrayFromHexString, uint8arrayToHexString, + base58Decode, + formatKey, additionalAssociatedData } }(); @@ -157,14 +234,14 @@ // TODO: deprecate EXTRACT_WALLET_ENCRYPTED_BUNDLE in favor of EXTRACT_ENCRYPTED_BUNDLE if (event.data && event.data["type"] == "EXTRACT_WALLET_ENCRYPTED_BUNDLE") { try { - await onExtractEncryptedBundle() + await onExtractWalletEncryptedBundle() } catch (e) { TKHQ.sendMessageUp("ERROR", e.toString()); } } - if (event.data && event.data["type"] == "EXTRACT_ENCRYPTED_BUNDLE") { + if (event.data && event.data["type"] == "EXTRACT_KEY_ENCRYPTED_BUNDLE") { try { - await onExtractEncryptedBundle() + await onExtractKeyEncryptedBundle(event.data["keyFormat"]) } catch (e) { TKHQ.sendMessageUp("ERROR", e.toString()); } @@ -202,8 +279,7 @@ } /** - * Function triggered when EXTRACT_ENCRYPTED_BUNDLE (and previously EXTRACT_WALLET_ENCRYPTED_BUNDLE) - * event is received. + * Function triggered when EXTRACT_WALLET_ENCRYPTED_BUNDLE event is received. * Prerequisite: This function uses the target public key in local storage that is imported * from the INJECT_IMPORT_BUNDLE event. * Uses the target public key in local storage to encrypt the text entered in the @@ -211,15 +287,18 @@ * an `encrypted_bundle` containing the ciphertext and encapped public key. * Example bundle: {"encappedPublic":"0497f33f3306f67f4402d4824e15b63b04786b6558d417aac2fef69051e46fa7bfbe776b142e4ded4f02097617a7588e93c53b71f900a4a8831a31be6f95e5f60f","ciphertext":"c17c3085505f3c094f0fa61791395b83ab1d8c90bdf9f12a64fc6e2e9cba266beb528f65c88bd933e36e6203752a9b63e6a92290a0ab6bf0ed591cf7bfa08006001e2cc63870165dc99ec61554ffdc14dea7d567e62cceed29314ae6c71a013843f5c06146dee5bf9c1d"} */ - const onExtractEncryptedBundle = async () => { + const onExtractWalletEncryptedBundle = async () => { // Get target embedded key from previous step (onInjectImportBundle) const targetPublicKeyJwk = TKHQ.getTargetEmbeddedKey(); if (targetPublicKeyJwk == null) { throw new Error("no target key found"); } - // Get plaintext wallet seedphrase or private key - const plaintext = document.getElementById("plaintext").value; + // Get plaintext wallet mnemonic + const plaintext = document.getElementById("plaintext").value.trim(); + if (!plaintext) { + throw new Error("no wallet mnemonic entered"); + } const plaintextBuf = new TextEncoder().encode(plaintext); // Encrypt the bundle using the enclave target public key @@ -233,6 +312,40 @@ TKHQ.sendMessageUp("ENCRYPTED_BUNDLE_EXTRACTED", encryptedBundle) } + /** + * Function triggered when EXTRACT_KEY_ENCRYPTED_BUNDLE event is received. + * Prerequisite: This function uses the target public key in local storage that is imported + * from the INJECT_IMPORT_BUNDLE event. + * Uses the target public key in local storage to encrypt the text entered in the + * `plaintext` textarea element. Upon successful encryption, sends + * an `encrypted_bundle` containing the ciphertext and encapped public key. + * Example bundle: {"encappedPublic":"0497f33f3306f67f4402d4824e15b63b04786b6558d417aac2fef69051e46fa7bfbe776b142e4ded4f02097617a7588e93c53b71f900a4a8831a31be6f95e5f60f","ciphertext":"c17c3085505f3c094f0fa61791395b83ab1d8c90bdf9f12a64fc6e2e9cba266beb528f65c88bd933e36e6203752a9b63e6a92290a0ab6bf0ed591cf7bfa08006001e2cc63870165dc99ec61554ffdc14dea7d567e62cceed29314ae6c71a013843f5c06146dee5bf9c1d"} + */ + const onExtractKeyEncryptedBundle = async keyFormat => { + // Get target embedded key from previous step (onInjectImportBundle) + const targetPublicKeyJwk = TKHQ.getTargetEmbeddedKey(); + if (targetPublicKeyJwk == null) { + throw new Error("no target key found"); + } + + // Get plaintext private key + const plaintext = document.getElementById("plaintext").value.trim(); + if (!plaintext) { + throw new Error("no private key entered"); + } + const plaintextBuf = TKHQ.formatKey(plaintext, keyFormat); + + // Encrypt the bundle using the enclave target public key + const encryptedBundle = await HpkeEncrypt( + { + plaintextBuf, + receiverPubJwk: targetPublicKeyJwk, + }); + + // Send up ENCRYPTED_BUNDLE_EXTRACTED message + TKHQ.sendMessageUp("ENCRYPTED_BUNDLE_EXTRACTED", encryptedBundle) + } + const HpkeEncrypt = async ({ plaintextBuf, receiverPubJwk }) => { const kemContext = new hpke.DhkemP256HkdfSha256(); const receiverPub = await kemContext.importKey("jwk", {...receiverPubJwk}, true); diff --git a/import/index.test.js b/import/index.test.js index 65871c2..fb9185d 100644 --- a/import/index.test.js +++ b/import/index.test.js @@ -51,6 +51,38 @@ describe("TKHQ", () => { expect(key.key_ops).toEqual([]); }) + it("formats hex-encoded private key correctly by default", async () => { + const keyHex = "0x13eff5b3f9c63eab5d53cff5149f01606b69325496e0e98b53afa938d890cd2e"; + const keyBytes = TKHQ.uint8arrayFromHexString(keyHex.slice(2)); + const formattedKey = TKHQ.formatKey(keyHex); + expect(formattedKey.length).toEqual(keyBytes.length); + for (let i = 0; i < formattedKey.length; i++) { + expect(formattedKey[i]).toEqual(keyBytes[i]); + } + }) + + it("parses hex-encoded private key correctly", async () => { + const keyHex = "0x13eff5b3f9c63eab5d53cff5149f01606b69325496e0e98b53afa938d890cd2e"; + const keyBytes = TKHQ.uint8arrayFromHexString(keyHex.slice(2)); + const formattedKey = TKHQ.formatKey(keyHex, "HEXADECIMAL"); + expect(formattedKey.length).toEqual(keyBytes.length); + for (let i = 0; i < formattedKey.length; i++) { + expect(formattedKey[i]).toEqual(keyBytes[i]); + } + }) + + it("parses solana private key correctly", async () => { + const keySol = "2P3qgS5A18gGmZJmYHNxYrDYPyfm6S3dJgs8tPW6ki6i2o4yx7K8r5N8CF7JpEtQiW8mx1kSktpgyDG1xuWNzfsM"; + const keyBytes = TKHQ.base58Decode(keySol); + expect(keyBytes.length).toEqual(64); + const keyPrivBytes = keyBytes.subarray(0, 32); + const formattedKey = TKHQ.formatKey(keySol, "SOLANA"); + expect(formattedKey.length).toEqual(keyPrivBytes.length); + for (let i = 0; i < formattedKey.length; i++) { + expect(formattedKey[i]).toEqual(keyPrivBytes[i]); + } + }) + it("contains additionalAssociatedData", async () => { // This is a trivial helper; concatenates the 2 arrays! expect(TKHQ.additionalAssociatedData(new Uint8Array([1, 2]), new Uint8Array([3, 4])).buffer).toEqual(new Uint8Array([1, 2, 3, 4]).buffer); diff --git a/import/standalone.html b/import/standalone.html index bf3b1d8..a4ff2a7 100644 --- a/import/standalone.html +++ b/import/standalone.html @@ -91,8 +91,6 @@

Message log

window.TKHQ = function() { /** constants for LocalStorage */ const TURNKEY_TARGET_EMBEDDED_KEY = "TURNKEY_TARGET_EMBEDDED_KEY" - /** 1 week in milliseconds */ - const TURNKEY_TARGET_EMBEDDED_KEY_TTL_IN_MILLIS = 1000 * 60 * 60 * 24 * 7; /* * Import a key to encrypt to as a CryptoKey and export it as a JSON Web Key. @@ -140,6 +138,84 @@

Message log

.join(''); } + /** + * Decodes a base58-encoded string into a buffer + * This function throws an error when the string contains invalid characters. + * @param {string} s The base58-encoded string. + * @return {Uint8Array} The decoded buffer. + */ + function base58Decode(s) { + // See https://en.bitcoin.it/wiki/Base58Check_encoding + var alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + var decoded = BigInt(0); + var decodedBytes = []; + var leadingZeros = []; + for (var i = 0; i < s.length; i++) { + if (alphabet.indexOf(s[i]) === -1) { + throw new Error(`cannot base58-decode: ${s[i]} isn't a valid character`) + } + var carry = alphabet.indexOf(s[i]); + + // If the current base58 digit is 0, append a 0 byte. + // "i == leadingZeros.length" can only be true if we have not seen non-zero bytes so far. + // If we had seen a non-zero byte, carry wouldn't be 0, and i would be strictly more than `leadingZeros.length` + if (carry == 0 && i === leadingZeros.length) { + leadingZeros.push(0); + } + + var j = 0; + while (j < decodedBytes.length || carry > 0) { + var currentByte = decodedBytes[j]; + + // shift the current byte 58 units and add the carry amount + // (or just add the carry amount if this is a new byte -- undefined case) + if (currentByte === undefined) { + currentByte = carry + } else { + currentByte = currentByte * 58 + carry + } + + // find the new carry amount (1-byte shift of current byte value) + carry = currentByte >> 8; + // reset the current byte to the remainder (the carry amount will pass on the overflow) + decodedBytes[j] = currentByte % 256; + j++ + } + } + + var result = leadingZeros.concat(decodedBytes.reverse()); + return new Uint8Array(result); + } + + /** + * Returns private key bytes from a private key, represented in + * the encoding and format specified by `keyFormat`. Defaults to + * hex-encoding if `keyFormat` isn't passed. + * @param {string} privateKey + * @param {string} keyFormat Can be "HEXADECIMAL" or "SOLANA" + */ + const formatKey = (privateKey, keyFormat) => { + switch (keyFormat) { + case "SOLANA": + const decodedKeyBytes = base58Decode(privateKey); + if (decodedKeyBytes.length !== 64) { + throw new Error(`invalid key length. Expected 64 bytes. Got ${decodedKeyBytes.length()}.`); + } + return decodedKeyBytes.subarray(0, 32); + case "HEXADECIMAL": + if (privateKey.startsWith("0x")) { + return uint8arrayFromHexString(privateKey.slice(2)); + } + return uint8arrayFromHexString(privateKey); + default: + console.warn(`invalid key format: ${keyFormat}. Defaulting to HEXADECIMAL.`); + if (privateKey.startsWith("0x")) { + return uint8arrayFromHexString(privateKey.slice(2)); + } + return uint8arrayFromHexString(privateKey); + } + } + /** * Additional Associated Data (AAD) in the format dictated by the enclave_encrypt crate. */ @@ -182,6 +258,8 @@

Message log

logMessage, uint8arrayFromHexString, uint8arrayToHexString, + base58Decode, + formatKey, additionalAssociatedData } }(); @@ -218,15 +296,15 @@

Message log

if (event.data && event.data["type"] == "EXTRACT_WALLET_ENCRYPTED_BUNDLE") { TKHQ.logMessage(`⬇️ Received message ${event.data["type"]}: ${event.data["value"]}`); try { - await onExtractEncryptedBundle(event.data["value"]) + await onExtractWalletEncryptedBundle(event.data["value"]) } catch (e) { TKHQ.sendMessageUp("ERROR", e.toString()); } } - if (event.data && event.data["type"] == "EXTRACT_ENCRYPTED_BUNDLE") { - TKHQ.logMessage(`⬇️ Received message ${event.data["type"]}: ${event.data["value"]}`); + if (event.data && event.data["type"] == "EXTRACT_KEY_ENCRYPTED_BUNDLE") { + TKHQ.logMessage(`⬇️ Received message ${event.data["type"]}: ${event.data["value"]}, ${event.data["keyFormat"]}`); try { - await onExtractEncryptedBundle(event.data["value"]) + await onExtractKeyEncryptedBundle(event.data["value"], event.data["keyFormat"]) } catch (e) { TKHQ.sendMessageUp("ERROR", e.toString()); } @@ -252,6 +330,15 @@

Message log

"value": document.getElementById("plaintext").value, }) }, false); + document.getElementById("encrypt-key-bundle").addEventListener("click", async e => { + e.preventDefault(); + window.postMessage({ + "type": "EXTRACT_KEY_ENCRYPTED_BUNDLE", + "value": document.getElementById("plaintext").value, + "keyFormat": document.getElementById("keyFormat").value, + "publicKey": document.getElementById("publicKey").value, + }) + }, false); }, false); /** @@ -292,15 +379,55 @@

Message log

* an `encrypted_bundle` containing the ciphertext and encapped public key. * Example bundle: {"encappedPublic":"0497f33f3306f67f4402d4824e15b63b04786b6558d417aac2fef69051e46fa7bfbe776b142e4ded4f02097617a7588e93c53b71f900a4a8831a31be6f95e5f60f","ciphertext":"c17c3085505f3c094f0fa61791395b83ab1d8c90bdf9f12a64fc6e2e9cba266beb528f65c88bd933e36e6203752a9b63e6a92290a0ab6bf0ed591cf7bfa08006001e2cc63870165dc99ec61554ffdc14dea7d567e62cceed29314ae6c71a013843f5c06146dee5bf9c1d"} */ - const onExtractEncryptedBundle = async bundle => { + const onExtractWalletEncryptedBundle = async bundle => { // Get target embedded key from previous step (onInjectImportBundle) const targetPublicKeyJwk = TKHQ.getTargetEmbeddedKey(); if (targetPublicKeyJwk == null) { throw new Error("no target key found"); } + // Get plaintext wallet mnemonic + const plaintext = bundle.trim(); + if (!plaintext) { + throw new Error("no wallet mnemonic entered"); + } + const plaintextBuf = new TextEncoder().encode(plaintext); + + // Encrypt the bundle using the enclave target public key + const encryptedBundle = await HpkeEncrypt( + { + plaintextBuf, + receiverPubJwk: targetPublicKeyJwk, + }); + + // Send up ENCRYPTED_BUNDLE_EXTRACTED message + TKHQ.sendMessageUp("ENCRYPTED_BUNDLE_EXTRACTED", encryptedBundle) + } + + /** + * Function triggered when EXTRACT_KEY_ENCRYPTED_BUNDLE event is received. + * Prerequisite: This function uses the target public key in local storage that is imported + * from the INJECT_IMPORT_BUNDLE event. + * Uses the target public key in local storage to encrypt the text entered in the + * `plaintext` textarea element. Upon successful encryption, sends + * an `encrypted_bundle` containing the ciphertext and encapped public key. + * Example bundle: {"encappedPublic":"0497f33f3306f67f4402d4824e15b63b04786b6558d417aac2fef69051e46fa7bfbe776b142e4ded4f02097617a7588e93c53b71f900a4a8831a31be6f95e5f60f","ciphertext":"c17c3085505f3c094f0fa61791395b83ab1d8c90bdf9f12a64fc6e2e9cba266beb528f65c88bd933e36e6203752a9b63e6a92290a0ab6bf0ed591cf7bfa08006001e2cc63870165dc99ec61554ffdc14dea7d567e62cceed29314ae6c71a013843f5c06146dee5bf9c1d"} + */ + const onExtractKeyEncryptedBundle = async (bundle, keyFormat) => { + // Get target embedded key from previous step (onInjectImportBundle) + const targetPublicKeyJwk = TKHQ.getTargetEmbeddedKey(); + if (targetPublicKeyJwk == null) { + throw new Error("no target key found"); + } + + // Get plaintext private key + const plaintext = bundle.trim(); + if (!plaintext) { + throw new Error("no private key entered"); + } + const plaintextBuf = TKHQ.formatKey(plaintext, keyFormat); + // Encrypt the bundle using the enclave target public key - const plaintextBuf = new TextEncoder().encode(bundle); const encryptedBundle = await HpkeEncrypt( { plaintextBuf,