zksync-sso
Version:
ZKsync Smart Sign On SDK
296 lines • 10.9 kB
JavaScript
import { ECDSASigValue } from "@peculiar/asn1-ecc";
import { AsnParser } from "@peculiar/asn1-schema";
import { bigintToBuf, bufToBigint } from "bigint-conversion";
import { Buffer } from "buffer";
import { encodeAbiParameters, pad, toHex } from "viem";
var COSEKEYS;
(function (COSEKEYS) {
COSEKEYS[COSEKEYS["kty"] = 1] = "kty";
COSEKEYS[COSEKEYS["alg"] = 3] = "alg";
COSEKEYS[COSEKEYS["crv"] = -1] = "crv";
COSEKEYS[COSEKEYS["x"] = -2] = "x";
COSEKEYS[COSEKEYS["y"] = -3] = "y";
})(COSEKEYS || (COSEKEYS = {}));
// Encode an integer in CBOR format
function encodeInt(int) {
if (int >= 0 && int <= 23) {
// Small positive integer (0–23)
return Buffer.from([int]);
}
else if (int >= 24 && int <= 255) {
// 1-byte positive integer
return Buffer.from([0x18, int]);
}
else if (int >= 256 && int <= 65535) {
// 2-byte positive integer
const buf = Buffer.alloc(3);
buf[0] = 0x19;
buf.writeUInt16BE(int, 1);
return buf;
}
else if (int < 0 && int >= -24) {
// Small negative integer (-1 to -24)
return Buffer.from([0x20 - (int + 1)]);
}
else if (int < -24 && int >= -256) {
// 1-byte negative integer
return Buffer.from([0x38, -int - 1]);
}
else if (int < -256 && int >= -65536) {
// 2-byte negative integer
const buf = Buffer.alloc(3);
buf[0] = 0x39;
buf.writeUInt16BE(-int - 1, 1);
return buf;
}
else {
throw new Error("Unsupported integer range");
}
}
// Encode a byte array in CBOR format
function encodeBytes(bytes) {
if (bytes.length <= 23) {
return Buffer.concat([Buffer.from([0x40 + bytes.length]), bytes]); // Byte array with small length
}
else if (bytes.length < 256) {
return Buffer.concat([Buffer.from([0x58, bytes.length]), bytes]); // Byte array with 1-byte length prefix
}
else {
throw new Error("Unsupported byte array length");
}
}
// Encode a map in CBOR format
function encodeMap(map) {
const encodedItems = [];
// CBOR map header, assuming the map size fits within small integer encoding
const mapHeader = 0xA0 | map.size;
encodedItems.push(Buffer.from([mapHeader]));
map.forEach((value, key) => {
// Encode the key
encodedItems.push(encodeInt(key));
// Encode the value based on its type (Buffer or number)
if (Buffer.isBuffer(value)) {
encodedItems.push(encodeBytes(value));
}
else {
encodedItems.push(encodeInt(value));
}
});
return Buffer.concat(encodedItems);
}
function decodeMap(buffer) {
const map = new Map();
let offset = 1; // Start after the map header
const mapHeader = buffer[0];
const mapSize = mapHeader & 0x1F; // Number of pairs
for (let i = 0; i < mapSize; i++) {
const [key, keyLength] = decodeInt(buffer, offset);
offset += keyLength;
const [value, valueLength] = decodeValue(buffer, offset);
offset += valueLength;
map.set(key, value);
}
return map;
}
function decodeInt(buffer, offset) {
const intByte = buffer[offset];
if (intByte < 24) {
// Small positive integer (0–23)
return [intByte, 1];
}
else if (intByte === 0x18) {
// 1-byte unsigned integer
return [buffer[offset + 1], 2];
}
else if (intByte === 0x19) {
// 2-byte unsigned integer
return [buffer.readUInt16BE(offset + 1), 3];
}
else if (intByte >= 0x20 && intByte <= 0x37) {
// Small negative integer (-1 to -24)
return [-(intByte - 0x20) - 1, 1];
}
else if (intByte === 0x38) {
// 1-byte negative integer
return [-1 - buffer[offset + 1], 2];
}
else if (intByte === 0x39) {
// 2-byte negative integer
return [-1 - buffer.readUInt16BE(offset + 1), 3];
}
else {
throw new Error("Unsupported integer format");
}
}
function decodeBytes(buffer, offset) {
const lengthByte = buffer[offset];
if (lengthByte >= 0x40 && lengthByte <= 0x57) {
const length = lengthByte - 0x40;
return [buffer.slice(offset + 1, offset + 1 + length), length + 1];
}
else if (lengthByte === 0x58) { // Byte array with 1-byte length prefix
const length = buffer[offset + 1];
return [buffer.slice(offset + 2, offset + 2 + length), length + 2];
}
else {
throw new Error("Unsupported byte format");
}
}
function decodeValue(buffer, offset) {
const type = buffer[offset];
if (type >= 0x40 && type <= 0x5F) { // Byte array
return decodeBytes(buffer, offset);
}
else {
return decodeInt(buffer, offset);
}
}
export const getPublicKeyBytesFromPasskeySignature = (publicPasskey) => {
const cosePublicKey = decodeMap(Buffer.from(publicPasskey)); // Decodes CBOR-encoded COSE key
const x = cosePublicKey.get(COSEKEYS.x);
const y = cosePublicKey.get(COSEKEYS.y);
return [Buffer.from(x), Buffer.from(y)];
};
export const getPasskeySignatureFromPublicKeyBytes = (coordinates) => {
const [xHex, yHex] = coordinates;
const x = Buffer.from(xHex.slice(2), "hex");
const y = Buffer.from(yHex.slice(2), "hex");
const cosePublicKey = new Map();
cosePublicKey.set(COSEKEYS.kty, 2); // Type 2 for EC keys
cosePublicKey.set(COSEKEYS.alg, -7); // -7 for ES256 algorithm
cosePublicKey.set(COSEKEYS.crv, 1); // Curve ID (1 for P-256)
cosePublicKey.set(COSEKEYS.x, x);
cosePublicKey.set(COSEKEYS.y, y);
const encodedPublicKey = encodeMap(cosePublicKey);
return new Uint8Array(encodedPublicKey);
};
/**
* Return 2 32byte words for the R & S for the EC2 signature, 0 l-trimmed
* @param signature
* @returns r & s bytes sequentially
*/
export function unwrapEC2Signature(signature) {
const parsedSignature = AsnParser.parse(signature, ECDSASigValue);
let rBytes = new Uint8Array(parsedSignature.r);
let sBytes = new Uint8Array(parsedSignature.s);
if (shouldRemoveLeadingZero(rBytes)) {
rBytes = rBytes.slice(1);
}
if (shouldRemoveLeadingZero(sBytes)) {
sBytes = sBytes.slice(1);
}
return {
r: rBytes,
s: normalizeS(sBytes),
};
}
/**
* Normalizes the 's' value of an ECDSA signature to prevent signature malleability.
*
* @param {Uint8Array} sBuf - The 's' value of the signature as a Uint8Array.
* @returns {Uint8Array} The normalized 's' value as a Uint8Array.
*
* @description
* This function implements the process of normalizing the 's' value in an ECDSA signature.
* It ensures that the 's' value is always in the lower half of the curve's order,
* which helps prevent signature malleability attacks.
*
* The function uses the curve order 'n' for secp256k1:
* n = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
*
* If 's' is greater than half of 'n', it is subtracted from 'n' to get the lower value.
*/
export function normalizeS(sBuf) {
const n = BigInt("0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551");
const halfN = n / BigInt(2);
const sNumber = bufToBigint(sBuf);
if (sNumber / halfN) {
return new Uint8Array(bigintToBuf(n - sNumber));
}
else {
return sBuf;
}
}
/**
* Determine if the DER-specific `00` byte at the start of an ECDSA signature byte sequence
* should be removed based on the following logic:
*
* "If the leading byte is 0x0, and the the high order bit on the second byte is not set to 0,
* then remove the leading 0x0 byte"
*/
function shouldRemoveLeadingZero(bytes) {
return bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0;
}
/**
* Decode from a Base64URL-encoded string to an ArrayBuffer. Best used when converting a
* credential ID from a JSON string to an ArrayBuffer, like in allowCredentials or
* excludeCredentials.
*
* @param buffer Value to decode from base64
* @param to (optional) The decoding to use, in case it's desirable to decode from base64 instead
*/
export function base64UrlToUint8Array(base64urlString, isUrl = true) {
const _buffer = toArrayBuffer(base64urlString, isUrl);
return new Uint8Array(_buffer);
}
function toArrayBuffer(data, isUrl) {
const
// Regular base64 characters
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
// Base64url characters
charsUrl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", genLookup = (target) => {
const lookupTemp = typeof Uint8Array === "undefined" ? [] : new Uint8Array(256);
const len = chars.length;
for (let i = 0; i < len; i++) {
lookupTemp[target.charCodeAt(i)] = i;
}
return lookupTemp;
},
// Use a lookup table to find the index.
lookup = genLookup(chars), lookupUrl = genLookup(charsUrl);
const len = data.length;
let bufferLength = data.length * 0.75, i, p = 0, encoded1, encoded2, encoded3, encoded4;
if (data[data.length - 1] === "=") {
bufferLength--;
if (data[data.length - 2] === "=") {
bufferLength--;
}
}
const arraybuffer = new ArrayBuffer(bufferLength), bytes = new Uint8Array(arraybuffer), target = isUrl ? lookupUrl : lookup;
for (i = 0; i < len; i += 4) {
encoded1 = target[data.charCodeAt(i)];
encoded2 = target[data.charCodeAt(i + 1)];
encoded3 = target[data.charCodeAt(i + 2)];
encoded4 = target[data.charCodeAt(i + 3)];
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
}
return arraybuffer;
}
;
export function passkeyHashSignatureResponseFormat(passkeyId, passkeyResponse, contracts) {
const signature = unwrapEC2Signature(base64UrlToUint8Array(passkeyResponse.signature));
const fatSignature = encodeAbiParameters([
{ type: "bytes" }, // authData
{ type: "bytes" }, // clientDataJson
{ type: "bytes32[2]" }, // signature (two elements)
{ type: "bytes" }, // unique passkey id
], [
toHex(base64UrlToUint8Array(passkeyResponse.authenticatorData)),
toHex(base64UrlToUint8Array(passkeyResponse.clientDataJSON)),
[pad(toHex(signature.r)), pad(toHex(signature.s))],
toHex(base64UrlToUint8Array(passkeyId)),
]);
const fullFormattedSig = encodeAbiParameters([
{ type: "bytes" }, // fat signature
{ type: "address" }, // validator address
{ type: "bytes[]" }, // validator data
], [
fatSignature,
contracts.passkey,
["0x"], // FIXME: this is assuming there are no other hooks
]);
return fullFormattedSig;
}
//# sourceMappingURL=passkey.js.map