Last active
December 26, 2023 10:07
-
-
Save oneleo/a01f80d26da000030e3a14cd32136b5e to your computer and use it in GitHub Desktop.
Secp256r1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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