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,