Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make raw p256 private key import work correctly #4

Merged
merged 3 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
265 changes: 240 additions & 25 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,7 @@ <h2>Message log</h2>
* @returns {Uint8Array}
*/
var uint8arrayFromHexString = function(hexString) {
var res = new Uint8Array(hexString.match(/../g).map(h=>parseInt(h,16)));
return res
return new Uint8Array(hexString.match(/../g).map(h=>parseInt(h,16)));
}

/**
Expand Down Expand Up @@ -198,13 +197,28 @@ <h2>Message log</h2>

/**
* Encodes a buffer into base64url
* @param {Uint8Array} byteArray
*/
function base64urlEncode(byteArray) {
return btoa(Array.from(new Uint8Array(byteArray)).map(val => {
return btoa(Array.from(byteArray).map(val => {
return String.fromCharCode(val);
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, '');
}

/**
* Encodes a buffer into base64url
* @param {string} s
* @return {Uint8Array}
*/
function base64urlDecode(s) {
var binaryString = atob(s.replace(/\-/g, '+').replace(/\_/g, '/'));
var bytes = new Uint8Array(binaryString.length);
for (var i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}

/**
* `SubtleCrypto.sign(...)` outputs signature in IEEE P1363 format:
* - https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#ecdsa
Expand Down Expand Up @@ -336,20 +350,47 @@ <h2>Message log</h2>
/**
* Returns a CryptoKey from a P256 private key bytes
* This is a bit awkward because webcrypto can't import raw private key bytes.
* We first convert these to pkcs8 bytes before importing.
* We use some custom crypto code to derive the public key from the private key bytes.
* Note that this is NOT security sensitive because browsers validate x/y coordinate
* when performing `crypto.subtle.importKey` operations.
* @param {Uint8Array} privateKeyBytes
*/
var importRecoveryCredential = async function(privateKeyBytes) {
var privateKeyHexString = uint8arrayToHexString(privateKeyBytes);
var privateKey = BigInt('0x' + privateKeyHexString);
var publicKeyPoint = P256Generator.multiply(privateKey);

// Specific byte-sequence for curve prime256v1 (DER encoding)
var hexString = "308141020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420" + privateKeyHexString
var pkcsBytes = new Uint8Array(hexString.match(/../g).map(h=>parseInt(h,16)))
//var pkcs8Bytes = bufferFromHexString();
return await window.crypto.subtle.importKey(
"jwk",
{
kty: "EC",
crv: "P-256",
d: bigIntToBase64Url(privateKey),
x: bigIntToBase64Url(publicKeyPoint.x.num),
y: bigIntToBase64Url(publicKeyPoint.y.num),
ext: true,
},
{
name: "ECDSA",
namedCurve: "P-256",
},
true,
["sign"]
)
}

var key = await window.crypto.subtle
.importKey("pkcs8", pkcsBytes, { name: "ECDSA", namedCurve: "P-256" }, true, ["sign"]);
return key
/**
* Converts a `BigInt` into a base64url encoded string
* @param {BigInt} num
* @return {string}
*/
var bigIntToBase64Url = function(num) {
var hexString = num.toString(16);
// Add an extra 0 to the start of the string to get a valid hex string (even length)
// (e.g. 0x0123 instead of 0x123)
var hexString = hexString.padStart(Math.ceil(hexString.length/2)*2, 0)
emostov marked this conversation as resolved.
Show resolved Hide resolved
var buffer = uint8arrayFromHexString(hexString);
return base64urlEncode(buffer)
}

/**
Expand All @@ -371,6 +412,186 @@ <h2>Message log</h2>
return compressedBytes
}

/**********************************************************************************************
* Start of private crypto implementation for P256 public key derivation from a private key.
* ----
* IMPORTANT NOTE: below we implement basic field arithmetic for P256
* This is only used to compute public point from a secret key inside of
* `importRecoveryCredential` above. If something goes wrong with the code below
* the web crypto API will simply refuse to import the key.
* None of the functions below are returned from the closure to minimize the risk of misuse.
*********************************************************************************************/

/**
* P256FieldElement represents a finite field element
* The field is set to be the P256 prime:
* 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
*/
var P256FieldElement = function(num) {
r-n-o marked this conversation as resolved.
Show resolved Hide resolved
this.num = BigInt(num);
this.prime = BigInt('0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff');
};
P256FieldElement.prototype.eq = function(other) {
return this.num === other.num;
}
P256FieldElement.prototype.add = function(other) {
num = this.num + other.num;
return new P256FieldElement(num % this.prime);
}
P256FieldElement.prototype.sub = function(other) {
res = (this.num - other.num) % this.prime;
if (res < BigInt(0)) { res += this.prime }
return new P256FieldElement(res);
}
P256FieldElement.prototype.mul = function(other) {
if (typeof(other) === "bigint") {
coefficient = other;
} else if (typeof(other) === "number") {
coefficient = BigInt(other)
} else {
coefficient = other.num;
r-n-o marked this conversation as resolved.
Show resolved Hide resolved
}
num = (this.num * coefficient) % this.prime
return new P256FieldElement(num);
}
P256FieldElement.prototype.div = function(other) {
// This uses fermat's little theorem (https://en.wikipedia.org/wiki/Fermat%27s_little_theorem)
// => if p is prime, then for any integer a: a**(p-1) % p = 1
// => we can compute inverses for any a: 1/a = a**(p-2) % p
return new P256FieldElement(other.num).pow(this.prime - BigInt(2)).mul(this.num)
}
P256FieldElement.prototype.pow = function(exponent) {
var exponent = BigInt(exponent);
var base = this.num % this.prime;
// Pretty standard double-and-add loop
var result = 1n;
while (exponent > BigInt(0)) {
if (exponent % BigInt(2)) {
result = (result * base) % this.prime;
}
exponent = exponent / BigInt(2);
base = (base * base) % this.prime;
}
return new P256FieldElement(result);
};

/**
* P256Point is a point (x, y) on the following elliptic curve:
* y**2 = x**3 + ax + b
* (where x and y are both finite field elements on the P256 field)
* https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-256
*
* We only define + and * since that's what's needed for public key derivation.
*/
P256Point = function(x, y) {
r-n-o marked this conversation as resolved.
Show resolved Hide resolved
this.x = x;
this.y = y;
this.a = new P256FieldElement(BigInt('0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc'));
this.b = new P256FieldElement(BigInt('0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b'));

if (this.x === null && this.y === null) {
// Point at infinity
return
}

var left = this.y.pow(2).num;
var right = this.x.pow(3).add(this.x.mul(this.a)).add(this.b).num;

if (left != right){
// y**2 = x**3 + 7 is the elliptic curve equation
throw new Error('Not on the P256 curve! y**2 (' + left + ') != x3 + ax + b (' + right + ')');
}
}

/**
* Addition is a complex operation because of the number of cases involved.
* The point at infinity is represented by (x=null, y=null), and represents the logical "0".
* So, to compute `A.add(B)`:
* - Case 1a: if A is 0, return B (0+B=B)
* - Case 1b: if B is 0, return A (A+0=A)
* - Case 2: if A and B have the same x but different y coordinates, they're
* opposite points: B is "-A". So, A+B=A+(-A)=0 (return point at infinity)
* - Case 3: if A and B are the same and at y=0, the A->B line is tangent to the y axis
* -> return point at infinity
* - Case 4: if A and B are the same (with y≠0), the formula for the result R (x3, y3):
* s = (3*x1**2 + a) / 2*y1
* x3 = s**2 - 2*x1
* y3 = s*(x1 - x3) - y1
* -> return (x3, y3)
* - Case 5: general case (different x coordinates). To get R (x3, y3) from A and B:
* s = (y2 -y1) / (x2 - x1)
* x3 = s**2 - x1 - x2
* y3 = s*(x1 - x3) - y1
* -> return (x3, y3)
*
* See https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Point_addition for helpful visuals
*/
P256Point.prototype.add = function(other) {
if (this.x === null) { return other; } /* 1a */
if (other.x === null) { return this; } /* 1b */

/* 2 */
if (this.x.eq(other.x) === true && this.y.eq(this.y) === false) {
return new P256Point(null, null);
}

/* 3 */
if (this.x.eq(other.x) && this.y.eq(other.y) && this.y.eq(new P256FieldElement(0))) {
return new P256Point(null, null);
}

/* 4 */
if (this.x.eq(other.x) && this.y.eq(other.y)) {
s = (this.x.pow(2).mul(3).add(this.a)).div(this.y.mul(2))
x = s.pow(2).sub(this.x.mul(2))
y = s.mul(this.x.sub(x)).sub(this.y)
return new P256Point(x, y)
}

/* 5 */
if (this.x.eq(other.x) === false) {
s = other.y.sub(this.y).div(other.x.sub(this.x));
x = s.pow(2).sub(this.x).sub(other.x);
y = s.mul(this.x.sub(x)).sub(this.y);
return new P256Point(x, y);
}

throw new Error('cannot handle addition of (' + this.x + ', ' + this.y + ') with (' + other.x + ', ' + other.y + ')');
}
/**
* Multiplication uses addition. Nothing crazy here.
* We start with "0" (point at infinity). Then we add increasing powers of 2.
* So, to multiply A by e.g. 25 (25 is 11001 in binary), we add 1*A, then compute
* 2*A, 4*A, 8*A by successive additions. Then add 8*A, then compute 16*A, then
* add 16*A.
*/
P256Point.prototype.multiply = function(coefficient) {
var coef = BigInt(coefficient);
var current = this;
var result = new P256Point(null, null);
while(coef) {
if (coef & BigInt(1)) {
result = result.add(current);
}
current = current.add(current);
coef >>= BigInt(1);
}
return result;
}

/**
* This is the P256 base point (aka generator)
* See https://www.secg.org/sec2-v2.pdf and https://neuromancer.sk/std/nist/P-256
*/
var P256Generator = new P256Point(
new P256FieldElement(BigInt('0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296')),
new P256FieldElement(BigInt('0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5'))
)

/**********************************************************************************************
* End of private crypto implementation for P256 public key derivation from a private key.
*********************************************************************************************/

return {
initEmbeddedKey,
generateTargetKey,
Expand All @@ -384,6 +605,7 @@ <h2>Message log</h2>
sendMessageUp,
logMessage,
base64urlEncode,
base64urlDecode,
stringToBase64urlString,
uint8arrayToHexString,
uint8arrayFromHexString,
Expand Down Expand Up @@ -453,25 +675,18 @@ <h2>Message log</h2>

/**
* Function triggered when INJECT_RECOVERY_BUNDLE event is received.
* The `bundle` param is the concatenation of a public key and an encrypted payload.
* For example:
* 04c1faba5812bde458fdbf505dc1fc9dc1eb3f7c8fbbc3284d399bcc8eef8d72f29baf967cd6f2b207005d86b74a51aff9525c553afbad4ac0835e47ffcf3623a2
* +
* 80b4a5708e2f95aa18dad9d90b8f49490893d1408a326c78abaa8a0d563ea7dbc28aaa35913f9d698be30d015afda49c
* ==
* 04c1faba5812bde458fdbf505dc1fc9dc1eb3f7c8fbbc3284d399bcc8eef8d72f29baf967cd6f2b207005d86b74a51aff9525c553afbad4ac0835e47ffcf3623a280b4a5708e2f95aa18dad9d90b8f49490893d1408a326c78abaa8a0d563ea7dbc28aaa35913f9d698be30d015afda49c
* The `bundle` param is the concatenation of a public key and an encrypted payload, and then base64 encoded
* Example: BNcKOotRE5od6mmnbLicwH29uUz_EbScMCU0EfmSZguTQIWC40kwdpmpFDwzYgr0jDgbSndeqPsX2kUpYQ_SCg-d1l7GSfiNZQ_P7MIgWtq2ZQx7jlbhNIyNW90fyHwcV_q3AwYl0qRlX7rHyoFRi98
* @param {string} bundle
*/
var onInjectBundle = async function(bundle) {
if (bundle.length <= 130) {
throw new Error("bundle size is too low. Expecting an uncompressed public key (130 chars) and an encrypted bundle!")
}
var bundleBytes = TKHQ.base64urlDecode(bundle);

var encappedKeyHex = bundle.substring(0, 130);
var encappedKeyBuf = TKHQ.uint8arrayFromHexString(encappedKeyHex);

var ciphertextHex = bundle.substring(130);
var ciphertextBuf = TKHQ.uint8arrayFromHexString(ciphertextHex);
var encappedKeyBuf = bundleBytes.subarray(0,65);
var ciphertextBuf = bundleBytes.subarray(65);

var embeddedKeyJwk = await TKHQ.getEmbeddedKey();
var recoveryCredentialBytes = await HpkeDecrypt(
Expand All @@ -481,7 +696,7 @@ <h2>Message log</h2>
receiverPrivJwk: embeddedKeyJwk,
});

RECOVERY_CREDENTIAL_BYTES = recoveryCredentialBytes;
RECOVERY_CREDENTIAL_BYTES = new Uint8Array(recoveryCredentialBytes);
TKHQ.sendMessageUp("BUNDLE_INJECTED", true)
}
/**
Expand Down Expand Up @@ -522,7 +737,7 @@ <h2>Message log</h2>
};

var stampHeaderValue = TKHQ.stringToBase64urlString(JSON.stringify(stamp));
sendMessageUp("STAMP", stampHeaderValue)
TKHQ.sendMessageUp("STAMP", stampHeaderValue)
}

/**
Expand Down
4 changes: 4 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ describe("TKHQ", () => {
expect(TKHQ.base64urlEncode(new Uint8Array([1, 2, 3]))).toEqual("AQID");
})

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

it("contains uint8arrayToHexString", () => {
expect(TKHQ.uint8arrayToHexString(new Uint8Array([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]))).toEqual("627566666572");
})
Expand Down