Skip to content

Instantly share code, notes, and snippets.

@oneleo
Last active December 26, 2023 10:07
Show Gist options
  • Save oneleo/a01f80d26da000030e3a14cd32136b5e to your computer and use it in GitHub Desktop.
Save oneleo/a01f80d26da000030e3a14cd32136b5e to your computer and use it in GitHub Desktop.
Secp256r1
import { AbiCoder } from "ethers";
import { readFileSync } from "fs";
import { p1363ToDer } from "./ecdsa-utils";
const userOpHash = process.argv[2];
const ethersAbi = AbiCoder.defaultAbiCoder();
const fido2Credential = JSON.parse(
readFileSync("test/ffi/bitwarden_export_20231219124736.json", "utf-8"),
).items[0]?.login?.fido2Credentials?.[0] as Fido2CredentialView;
const KeyUsages: KeyUsage[] = ["sign"];
const isNode =
typeof process !== "undefined" &&
(process as any).release != null &&
(process as any).release.name === "node";
const main = () => {
p256();
};
const p256 = async () => {
if (!userOpHash) {
console.error("Usage: ts-node p256.ts <userOpHash>");
process.exit(1);
}
console.log(`userOpHash: ${userOpHash}`);
// % npx ts-node test/ffi/p256.ts 0xe6bdbae2879ecdae390c002716048d2f26f2a46b18eb819e21ad82e54a9b9919
// → 5r264oeeza45DAAnFgSNLybypGsY64GeIa2C5UqbmRk
const userOpHashBuffer = fromHexToArray(userOpHash.slice(2));
const userOpHashUrlB64 = fromBufferToUrlB64(userOpHashBuffer);
console.log(`userOpHashUrlB64: ${userOpHashUrlB64}`);
const authenticatorData = fromUrlB64ToArray(
"T7IIVvJKaufa_CeBCQrIR3rm4r0HJmAjbMYUxvt8LqAFAAAAAQ==",
);
const clientDataJson = JSON.stringify({
type: `webauthn.get`,
challenge: `${userOpHashUrlB64}`,
origin: `https://webauthn.passwordless.id`,
crossOrigin: false,
});
console.log(`clientDataJson: ${clientDataJson}`);
const clientDataJsonHash = await crypto.subtle.digest(
{ name: "SHA-256" },
fromByteStringToArray(clientDataJson),
);
// let keyPair: CryptoKeyPair;
let priKey: CryptoKey;
console.log(`fido2Credential: ${JSON.stringify(fido2Credential)}`);
if (fido2Credential) {
priKey = await getPrivateKeyFromFido2Credential(fido2Credential);
const priKeyPkcs8 = await crypto.subtle.exportKey("pkcs8", priKey);
console.log(`priKeyPkcs8: ${bufferToString(priKeyPkcs8)}`);
} else {
const keyPair = await createKeyPair();
priKey = keyPair.privateKey;
const priKeyPkcs8 = await crypto.subtle.exportKey(
"pkcs8",
keyPair.privateKey,
);
const pubKeyJwk = await crypto.subtle.exportKey(
"jwk",
keyPair.publicKey,
);
const pubKeyXHex = `0x${fromBufferToHex(
fromUrlB64ToArray(pubKeyJwk.x),
)}`;
const pubKeyXUint = ethersAbi.decode(
["uint256"],
ethersAbi.encode(["bytes32"], [pubKeyXHex]),
)[0] as bigint;
const pubKeyYHex = `0x${fromBufferToHex(
fromUrlB64ToArray(pubKeyJwk.y),
)}`;
const pubKeyYUint = ethersAbi.decode(
["uint256"],
ethersAbi.encode(["bytes32"], [pubKeyYHex]),
)[0] as bigint;
console.log(`priKeyPkcs8: ${bufferToString(priKeyPkcs8)}`);
console.log(`pubKeyX: ${pubKeyXUint}`);
console.log(`pubKeyY: ${pubKeyYUint}`);
}
const sigBase = new Uint8Array([
...authenticatorData,
...bufferSourceToUint8Array(clientDataJsonHash),
]);
const p1363_signature = new Uint8Array(
await crypto.subtle.sign(
{
name: "ECDSA",
hash: { name: "SHA-256" },
},
priKey,
sigBase,
),
);
console.log(`p1363_signature: ${bufferToString(p1363_signature)}`);
const asn1Der_signature = p1363ToDer(p1363_signature);
console.log(`asn1Der_signature: ${bufferToString(asn1Der_signature)}`);
};
// ----------------
// -- Interfaces --
// ----------------
interface Fido2CredentialView {
credentialId: string;
keyType: "public-key";
keyAlgorithm: "ECDSA";
keyCurve: "P-256";
keyValue: string;
rpId: string;
userHandle: string;
userName: string;
counter: number;
rpName: string;
userDisplayName: string;
discoverable: boolean;
creationDate: Date;
}
// ----------------
// --- Helpers ---
// ----------------
const createKeyPair = async (): Promise<CryptoKeyPair> => {
return await crypto.subtle.generateKey(
{
name: "ECDSA",
namedCurve: "P-256",
},
true,
KeyUsages,
);
};
const getPrivateKeyFromFido2Credential = async (
fido2Credential: Fido2CredentialView,
): Promise<CryptoKey> => {
const keyBuffer = fromUrlB64ToArray(fido2Credential.keyValue);
return await crypto.subtle.importKey(
"pkcs8",
keyBuffer,
{
name: fido2Credential.keyAlgorithm,
namedCurve: fido2Credential.keyCurve,
} as EcKeyImportParams,
true,
KeyUsages,
);
};
// ----------------
// -- Converters --
// ----------------
const fromB64ToArray = (str: string): Uint8Array => {
if (str == null) {
return null;
}
if (isNode) {
return new Uint8Array(Buffer.from(str, "base64"));
} else {
const binaryString = global.atob(str);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
};
const fromHexToArray = (str: string): Uint8Array => {
if (isNode) {
return new Uint8Array(Buffer.from(str, "hex"));
} else {
const bytes = new Uint8Array(str.length / 2);
for (let i = 0; i < str.length; i += 2) {
bytes[i / 2] = parseInt(str.substr(i, 2), 16);
}
return bytes;
}
};
const fromByteStringToArray = (str: string): Uint8Array => {
if (str == null) {
return null;
}
const arr = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
arr[i] = str.charCodeAt(i);
}
return arr;
};
const fromUrlB64ToArray = (str: string): Uint8Array => {
return fromB64ToArray(fromUrlB64ToB64(str));
};
const bufferToString = (bufferSource: BufferSource): string => {
const buffer = bufferSourceToUint8Array(bufferSource);
return fromBufferToUrlB64(buffer);
};
const bufferSourceToUint8Array = (bufferSource: BufferSource) => {
if (isArrayBuffer(bufferSource)) {
return new Uint8Array(bufferSource);
} else {
return new Uint8Array(bufferSource.buffer);
}
};
const isArrayBuffer = (
bufferSource: BufferSource,
): bufferSource is ArrayBuffer => {
return (
bufferSource instanceof ArrayBuffer || bufferSource.buffer === undefined
);
};
const fromUrlB64ToB64 = (urlB64Str: string): string => {
let output = urlB64Str.replace(/-/g, "+").replace(/_/g, "/");
switch (output.length % 4) {
case 0:
break;
case 2:
output += "==";
break;
case 3:
output += "=";
break;
default:
throw new Error("Illegal base64url string!");
}
return output;
};
const fromBufferToUrlB64 = (buffer: ArrayBuffer): string => {
return fromB64toUrlB64(fromBufferToB64(buffer));
};
const fromBufferToB64 = (buffer: ArrayBuffer): string => {
if (buffer == null) {
return null;
}
if (isNode) {
return Buffer.from(buffer).toString("base64");
} else {
let binary = "";
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return global.btoa(binary);
}
};
const fromBufferToHex = (buffer: ArrayBuffer): string => {
if (isNode) {
return Buffer.from(buffer).toString("hex");
} else {
const bytes = new Uint8Array(buffer);
return Array.prototype.map
.call(bytes, (x: number) => ("00" + x.toString(16)).slice(-2))
.join("");
}
};
const fromB64toUrlB64 = (b64Str: string) => {
return b64Str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
};
// ----------------
// ----- Main -----
// ----------------
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment