Skip to content

Commit

Permalink
Merge pull-request #18
Browse files Browse the repository at this point in the history
  • Loading branch information
r-n-o committed Nov 20, 2023
2 parents 1adaa70 + e280594 commit 787b1fc
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 4 deletions.
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
18.18.2
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ git clone [email protected]:tkhq/frames.git
cd frames/
```

Install dependencies
Install Node:
```sh
nvm use
```
(the command above installs the version specified in `.nvmrc`, but any Node version >= v18 should do)

Install dependencies:
```sh
cd recovery && npm install
cd export && npm install
Expand Down
90 changes: 88 additions & 2 deletions recovery/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ <h2>Message log</h2>
}

/**
* Encodes a buffer into base64url
* Decodes a base64-encoded string into a buffer
* @param {string} s
* @return {Uint8Array}
*/
Expand All @@ -262,6 +262,72 @@ <h2>Message log</h2>
return bytes;
}

/**
* Decodes a base58check-encoded string into a buffer
* This function throws an error when the string is too small, doesn't have the proper checksum, or contains invalid characters.
* Inspired by https://gist.github.com/diafygi/90a3e80ca1c2793220e5/
* @param {string} s
* @return {Uint8Array}
*/
async function base58checkDecode(s) {
if (s.length < 5) {
throw new Error(`cannot base58-decode a string of length < 5 (found length ${s.length})`)
}

// 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());

var foundChecksum = result.slice(result.length - 4)

var msg = result.slice(0, result.length - 4)
var checksum1 = await crypto.subtle.digest("SHA-256", new Uint8Array(msg))
var checksum2 = await crypto.subtle.digest("SHA-256", new Uint8Array(checksum1))
var computedChecksum = Array.from(new Uint8Array(checksum2)).slice(0, 4);

if (computedChecksum.toString() != foundChecksum.toString()) {
throw new Error(`checksums do not match: computed ${computedChecksum} but found ${foundChecksum}`)
}

return new Uint8Array(msg);
}

/**
* `SubtleCrypto.sign(...)` outputs signature in IEEE P1363 format:
* - https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#ecdsa
Expand Down Expand Up @@ -764,6 +830,7 @@ <h2>Message log</h2>
logMessage,
base64urlEncode,
base64urlDecode,
base58checkDecode,
bigIntToHex,
stringToBase64urlString,
uint8arrayToHexString,
Expand Down Expand Up @@ -854,7 +921,26 @@ <h2>Message log</h2>
* @param {string} bundle
*/
var onInjectBundle = async function(bundle) {
var bundleBytes = TKHQ.base64urlDecode(bundle);
if (
// Non-alphanumerical characters in base64url: - and _. These aren't in base58.
bundle.indexOf("-") === -1 && bundle.indexOf("_") === -1
// Uppercase o (O), uppercase i (I), lowercase L (l), and 0 aren't in the character set either.
&& bundle.indexOf("O") === -1 && bundle.indexOf("I") === -1 && bundle.indexOf("l") === -1 && bundle.indexOf("0") === -1
) {
// If none of these characters are in the bundle we assume it's a base58check-encoded string
// This isn't perfect: there's a small chance that a base64url-encoded string doesn't have any of these characters by chance!
// But we accept this risk given this branching is only here to support our transition to base58check.
// I hear you'd like to quantify this risk? Let's do it.
// Assuming random bytes in our bundle and a bundle length of 33 (public key, compressed) + 48 (encrypted cred) = 81 bytes.
// The odds of a byte being in the overlap set between base58 and base64url is 58/64=0.90625.
// Which means the odds of a 81 bytes string being in the overlap character set for its entire length is...
// ... 0.90625^81 = 0.0003444209703
// Are you convinced that this is good enough? I am :)
var bundleBytes = await TKHQ.base58checkDecode(bundle);
} else {
var bundleBytes = TKHQ.base64urlDecode(bundle);
}

if (bundleBytes.byteLength <= 33) {
throw new Error("bundle size " + bundleBytes.byteLength + " is too low. Expecting a compressed public key (33 bytes) and an encrypted credential")
}
Expand Down
23 changes: 22 additions & 1 deletion recovery/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,28 @@ describe("TKHQ", () => {
})

it("contains base64urlDecode", () => {
expect(TKHQ.base64urlDecode("AQID").buffer).toEqual(new Uint8Array([1, 2, 3]).buffer);
expect(Array.from(TKHQ.base64urlDecode("AQID"))).toEqual([1, 2, 3]);
})

it("contains base58checkDecode", async () => {
await expect(TKHQ.base58checkDecode("N0PE")).rejects.toThrow("cannot base58-decode a string of length < 5 (found length 4)");
await expect(TKHQ.base58checkDecode("NOOOO")).rejects.toThrow("cannot base58-decode: O isn't a valid character");

// Satoshi's Bitcoin address
expect(Array.from(await TKHQ.base58checkDecode("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"))).toEqual(
// Note: checksum is missing from this expected value since we chop the checksum as part of decoding.
// Decoded value on http://lenschulwitz.com/base58 has C29B7D93 (4 bytes) at the end, that's expected and normal.
Array.from(TKHQ.uint8arrayFromHexString("0062E907B15CBF27D5425399EBF6F0FB50EBB88F18"))
);

// Same input as above, except last digit changed.
await expect(TKHQ.base58checkDecode("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNb")).rejects.toThrow("checksums do not match: computed 194,155,125,147 but found 194,155,125,148");

// Realistic recovery code: concatenation of a 33 bytes P-256 public key + a 48-bytes long encrypted credential
// Test vector from our internal repo, which uses Rust to encode in base58check.
expect(Array.from(await TKHQ.base58checkDecode("szrFBNGDkhXyVvRoqjjDT6xd7kRhDXHmtQH3NVkPuVVkeiPFjn6UkyjbiTzuxH9wKH4QdEJUaWxZLM1ZLzByUFN1TNjxVh5aoZENCnKYrSEdZBnRWcK"))).toEqual(
Array.from(TKHQ.uint8arrayFromHexString("02cb30f1f44d411383cc2a7bb7135d87e0fbf265d0e002b460c9d38d97b14cd0d26114254d213cd77887293644d942a62516a3f174f01ed1ccb57dea1f8ac88664759bb6febcd8b060e7a11d23c614dd66"))
);
})

it("contains uint8arrayToHexString", () => {
Expand Down

0 comments on commit 787b1fc

Please sign in to comment.