Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 281 additions & 1 deletion export/index.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,233 @@ <h2>Message log</h2>
/** 48 hours in milliseconds */
const TURNKEY_EMBEDDED_KEY_TTL_IN_MILLIS = 1000 * 60 * 60 * 48;

const BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";

// Map char -> value
const BECH32_CHAR_MAP = (() => {
const map = {};
for (let i = 0; i < BECH32_CHARSET.length; i++) {
map[BECH32_CHARSET[i]] = i;
}
return map;
})();

/**
* Internal function that computes the Bech32 checksum.
* @param {number[]} values
* @returns {number}
*/
function bech32Polymod(values) {
let chk = 1;
const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
for (let p = 0; p < values.length; p++) {
const v = values[p];
const top = chk >>> 25;
chk = ((chk & 0x1ffffff) << 5) ^ v;
for (let i = 0; i < 5; i++) {
if (((top >>> i) & 1) !== 0) {
chk ^= GEN[i];
}
}
}
return chk;
}

/**
* Expand the HRP for Bech32 checksum computation.
* @param {string} hrp
* @returns {number[]}
*/
function bech32HrpExpand(hrp) {
const ret = [];
for (let i = 0; i < hrp.length; i++) {
const c = hrp.charCodeAt(i);
ret.push(c >> 5);
}
ret.push(0);
for (let i = 0; i < hrp.length; i++) {
const c = hrp.charCodeAt(i);
ret.push(c & 31);
}
return ret;
}

/**
* Create a Bech32 checksum.
* @param {string} hrp
* @param {number[]} data
* @returns {number[]}
*/
function bech32CreateChecksum(hrp, data) {
const values = bech32HrpExpand(hrp).concat(data);
const polymod = bech32Polymod(values.concat([0, 0, 0, 0, 0, 0])) ^ 1; // bech32 const = 1
const checksum = [];
for (let i = 0; i < 6; i++) {
checksum.push((polymod >> (5 * (5 - i))) & 31);
}
return checksum;
}

/**
* Convert from 8-bit bytes to 5-bit words.
* @param {Uint8Array} bytes
* @returns {number[]}
*/
function bech32ToWords(bytes) {
const fromBits = 8;
const toBits = 5;
const pad = true;

let acc = 0;
let bits = 0;
const maxv = (1 << toBits) - 1;
const result = [];

for (let i = 0; i < bytes.length; i++) {
const value = bytes[i];
if (value < 0 || value > 255) {
throw new Error("invalid byte value for bech32");
}
acc = (acc << fromBits) | value;
bits += fromBits;
while (bits >= toBits) {
bits -= toBits;
result.push((acc >> bits) & maxv);
}
}

if (pad) {
if (bits > 0) {
result.push((acc << (toBits - bits)) & maxv);
}
} else {
if (bits >= fromBits) {
throw new Error("excess padding in bech32 data");
}
if ((acc & ((1 << bits) - 1)) !== 0) {
throw new Error("non-zero padding in bech32 data");
}
}

return result;
}

/**
* Convert from 5-bit words to 8-bit bytes.
* @param {number[]} words
* @returns {Uint8Array}
*/
function bech32FromWords(words) {
const fromBits = 5;
const toBits = 8;
const pad = false;

let acc = 0;
let bits = 0;
const maxv = (1 << toBits) - 1;
const maxAcc = (1 << (fromBits + toBits - 1)) - 1;
const result = [];

for (let i = 0; i < words.length; i++) {
const value = words[i];
if (value < 0 || (value >> fromBits) !== 0) {
throw new Error("invalid bech32 word");
}
acc = ((acc << fromBits) | value) & maxAcc;
bits += fromBits;
while (bits >= toBits) {
bits -= toBits;
result.push((acc >> bits) & maxv);
}
}

if (pad) {
if (bits > 0) {
result.push((acc << (toBits - bits)) & maxv);
}
} else {
if (bits >= fromBits) {
throw new Error("excess padding in bech32 data");
}
if ((acc & ((1 << bits) - 1)) !== 0) {
throw new Error("non-zero padding in bech32 data");
}
}

return new Uint8Array(result);
}

/**
* Encode bytes into a Bech32 string.
* @param {string} hrp
* @param {Uint8Array} bytes
* @returns {string}
*/
function encodeBech32(hrp, bytes) {
const words = bech32ToWords(bytes);
const checksum = bech32CreateChecksum(hrp, words);
const combined = words.concat(checksum);

let out = hrp + "1";
for (let i = 0; i < combined.length; i++) {
out += BECH32_CHARSET[combined[i]];
}
return out;
}

/**
* Strict decode: accepts bech32 or bech32m.
* @param {string} str
* @returns {{hrp: string, words: number[], variant: string}}
*/
function decodeBech32(str) {
// Enforce lower-case (Sui uses lowercase)
const s = str.toLowerCase();

// Split HRP and data at last '1'
const pos = s.lastIndexOf("1");
if (pos <= 0 || pos + 7 > s.length) {
throw new Error("invalid bech32: bad separator or too short");
}

const hrp = s.slice(0, pos);
const dataPart = s.slice(pos + 1);

if (!hrp || hrp.length < 1) {
throw new Error("invalid bech32: HRP too short");
}

const data = [];
for (let i = 0; i < dataPart.length; i++) {
const c = dataPart[i];
const v = BECH32_CHAR_MAP[c];
if (v == null) {
throw new Error(`invalid bech32 character: '${c}'`);
}
data.push(v);
}

if (data.length < 6) {
throw new Error("invalid bech32: data too short (no room for checksum)");
}

const values = bech32HrpExpand(hrp).concat(data);
const polymod = bech32Polymod(values);

// bech32 checksum = 1, bech32m checksum = 0x2bc830a3
const BECH32_CONST = 1;
const BECH32M_CONST = 0x2bc830a3;

if (polymod !== BECH32_CONST && polymod !== BECH32M_CONST) {
throw new Error("invalid bech32: checksum mismatch");
}

// strip checksum (last 6 words)
const words = data.slice(0, data.length - 6);

return { hrp, words, variant: polymod === BECH32_CONST ? "bech32" : "bech32m" };
}

var parentFrameMessageChannelPort = null;

/*
Expand Down Expand Up @@ -593,6 +820,29 @@ <h2>Message log</h2>
return result;
}

/**
* Decodes a base58-check-encoded string into a buffer
* This function throws an error when the string contains invalid characters
* or the checksum is invalid.
* @param {string} s The base58-check-encoded string.
* @return {Uint8Array} The decoded buffer.
*/
async function base58CheckEncode(payload) {
const hash1Buf = await crypto.subtle.digest("SHA-256", payload);
const hash1 = new Uint8Array(hash1Buf);

const hash2Buf = await crypto.subtle.digest("SHA-256", hash1);
const hash2 = new Uint8Array(hash2Buf);

const checksum = hash2.slice(0, 4);

const full = new Uint8Array(payload.length + 4);
full.set(payload, 0);
full.set(checksum, payload.length);

return base58Encode(full);
}

/**
* Decodes a base58-encoded string into a buffer
* This function throws an error when the string contains invalid characters.
Expand Down Expand Up @@ -650,7 +900,7 @@ <h2>Message log</h2>
* 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} keyFormat Can be "HEXADECIMAL", "SUI_BECH32", "BITCOIN_WIF" or "SOLANA"
*/
async function encodeKey(privateKeyBytes, keyFormat, publicKeyBytes) {
switch (keyFormat) {
Expand All @@ -676,6 +926,33 @@ <h2>Message log</h2>
return base58Encode(concatenatedBytes);
case "HEXADECIMAL":
return "0x" + uint8arrayToHexString(privateKeyBytes);
case "BITCOIN_WIF":
if (privateKeyBytes.length !== 32) {
throw new Error(
`invalid private key length. Expected 32 bytes. Got ${privateKeyBytes.length}.`
);
}

const version = 0x80 // mainnet version byte TODO: support testnet? how would we know which to use?
const wifPayload = new Uint8Array(1 + 32 + 1);
wifPayload[0] = version;
wifPayload.set(privateKeyBytes, 1);
wifPayload[33] = 0x01 // compressed flag

return await base58CheckEncode(wifPayload);
case "SUI_BECH32":
if (privateKeyBytes.length !== 32) {
throw new Error(
`invalid private key length. Expected 32 bytes. Got ${privateKeyBytes.length}.`
);
}

const schemeFlag = 0x00; // ED25519 | We only support ED25519 keys for Sui currently
const bech32Payload = new Uint8Array(1 + 32);
bech32Payload[0] = schemeFlag;
bech32Payload.set(privateKeyBytes, 1);

return encodeBech32("suiprivkey", bech32Payload);
default:
console.warn(
`invalid key format: ${keyFormat}. Defaulting to HEXADECIMAL.`
Expand Down Expand Up @@ -842,7 +1119,10 @@ <h2>Message log</h2>
p256JWKPrivateToPublic,
base58Encode,
base58Decode,
decodeBech32,
bech32FromWords,
encodeKey,
encodeBech32,
encodeWallet,
sendMessageUp,
logMessage,
Expand Down
19 changes: 19 additions & 0 deletions export/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,25 @@ describe("TKHQ", () => {
expect(encodedKey).toEqual(keySol);
});

it("encodes bitcoin WIF private key correctly", async () => {
const keyWif = "L1sF5SF3CnCN9gA7vh7MAtbiVu9igdr3C1BYPKZduw4yaezdeCTV";
const keyWifBytes = (TKHQ.base58Decode(keyWif)).subarray(0, -4); // remove 4 byte checksum
expect(keyWifBytes.length).toEqual(34); // 1 byte version + 32 byte privkey + 1 byte compressed flag
const keyPrivBytes = keyWifBytes.subarray(1, 33);
const encodedKey = await TKHQ.encodeKey(keyPrivBytes, "BITCOIN_WIF");
expect(encodedKey).toEqual(keyWif);
});

it("encodes sui bech32 private key correctly", async () => {
const keySui =
"suiprivkey1qpj5xd9396rxsu7h45tzccalhuf95e4pygls3ps9txszn9ywpwsnznaeq0l";
const { _, words} = TKHQ.decodeBech32(keySui);
const keySuiBytes = (TKHQ.bech32FromWords(words)).subarray(1); // remove 1 byte scheme flag
expect(keySuiBytes.length).toEqual(32);
const encodedKey = await TKHQ.encodeKey(keySuiBytes, "SUI_BECH32");
expect(encodedKey).toEqual(keySui);
});

it("encodes wallet with only mnemonic correctly", async () => {
const mnemonic =
"suffer surround soup duck goose patrol add unveil appear eye neglect hurry alpha project tomorrow embody hen wish twenty join notable amused burden treat";
Expand Down
Loading
Loading