Skip to content

Commit

Permalink
Add optional keyFormat and publicKey parameters to injectKeyExportBun…
Browse files Browse the repository at this point in the history
…dle. Add extractKeyEncryptedBundle.
  • Loading branch information
Olivia Thet committed Mar 13, 2024
1 parent 09846aa commit 7728174
Show file tree
Hide file tree
Showing 5 changed files with 447 additions and 41 deletions.
150 changes: 138 additions & 12 deletions export/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -81,16 +84,27 @@ <h2>Export Key Material</h2>
<br>
<br>
<br>
<h2>Inject Export Bundle</h2>
<h2>Inject Key Export Bundle</h2>
<p><em>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 <a target="_blank" href="https://datatracker.ietf.org/doc/rfc9180/">HPKE (RFC 9180)</a></em>.</p>
<form>
<label>Key Bundle</label>
<label>Bundle</label>
<input type="text" name="key-export-bundle" id="key-export-bundle"/>
<button id="inject-key">Inject Bundle</button>
<br>
<label>Key Format</label>
<select id="key-export-format" name="exampleDropdown">
<option value="HEXADECIMAL">Hexadecimal (Default)</option>
<option value="SOLANA">Solana</option>
</select>
<br>
<label>Public Key (Optional)</label>
<input type="text" name="key-export-public-key" id="key-export-public-key"/>
</form>
<br>
<h2>Inject Wallet Export Bundle</h2>
<p><em>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 <a target="_blank" href="https://datatracker.ietf.org/doc/rfc9180/">HPKE (RFC 9180)</a></em>.</p>
<form>
<label>Wallet Bundle</label>
<label>Bundle</label>
<input type="text" name="wallet-export-bundle" id="wallet-export-bundle"/>
<button id="inject-wallet">Inject Bundle</button>
</form>
Expand Down Expand Up @@ -268,12 +282,120 @@ <h2>Message log</h2>
}

/**
* 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);
}
}

/**
Expand Down Expand Up @@ -310,6 +432,8 @@ <h2>Message log</h2>
setEmbeddedKey,
onResetEmbeddedKey,
p256JWKPrivateToPublic,
base58Encode,
base58Decode,
parseKey,
parseWallet,
sendMessageUp,
Expand Down Expand Up @@ -342,9 +466,9 @@ <h2>Message log</h2>
// 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());
}
Expand Down Expand Up @@ -377,6 +501,8 @@ <h2>Message log</h2>
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 => {
Expand Down Expand Up @@ -447,12 +573,12 @@ <h2>Message log</h2>
* 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);
Expand Down
28 changes: 18 additions & 10 deletions export/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Loading

0 comments on commit 7728174

Please sign in to comment.