UNPKG

@turnkey/core

Version:

A core JavaScript web and React Native package for interfacing with Turnkey's infrastructure.

203 lines (199 loc) 7.53 kB
'use strict'; var encoding = require('@turnkey/encoding'); var utils = require('../../../utils.js'); var apiKeyStamper = require('@turnkey/api-key-stamper'); const DB_NAME = "TurnkeyStamperDB"; const DB_STORE = "KeyStore"; const stampHeaderName = "X-Stamp"; /** * `SubtleCrypto.sign(...)` outputs signature in IEEE P1363 format: * - https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#ecdsa * * Turnkey expects the signature encoding to be DER-encoded ASN.1: * - https://github.com/tkhq/tkcli/blob/7f0159af5a73387ff050647180d1db4d3a3aa033/src/internal/apikey/apikey.go#L149 * * Code modified from https://github.com/google/tink/blob/6f74b99a2bfe6677e3670799116a57268fd067fa/javascript/subtle/elliptic_curves.ts#L114 * * Transform an ECDSA signature in IEEE 1363 encoding to DER encoding. * * @param ieee the ECDSA signature in IEEE encoding * @return ECDSA signature in DER encoding */ function convertEcdsaIeee1363ToDer(ieee) { if (ieee.length % 2 != 0 || ieee.length == 0 || ieee.length > 132) { throw new Error("Invalid IEEE P1363 signature encoding. Length: " + ieee.length); } const r = toUnsignedBigNum(ieee.subarray(0, ieee.length / 2)); const s = toUnsignedBigNum(ieee.subarray(ieee.length / 2, ieee.length)); let offset = 0; const length = 1 + 1 + r.length + 1 + 1 + s.length; let der; if (length >= 128) { der = new Uint8Array(length + 3); der[offset++] = 48; der[offset++] = 128 + 1; der[offset++] = length; } else { der = new Uint8Array(length + 2); der[offset++] = 48; der[offset++] = length; } der[offset++] = 2; der[offset++] = r.length; der.set(r, offset); offset += r.length; der[offset++] = 2; der[offset++] = s.length; der.set(s, offset); return der; } /** * Code modified from https://github.com/google/tink/blob/6f74b99a2bfe6677e3670799116a57268fd067fa/javascript/subtle/elliptic_curves.ts#L311 * * Transform a big integer in big endian to minimal unsigned form which has * no extra zero at the beginning except when the highest bit is set. */ function toUnsignedBigNum(bytes) { let start = 0; while (start < bytes.length && bytes[start] == 0) { start++; } if (start == bytes.length) { start = bytes.length - 1; } let extraZero = 0; if ((bytes[start] & 128) == 128) { extraZero = 1; } const res = new Uint8Array(bytes.length - start + extraZero); res.set(bytes.subarray(start), extraZero); return res; } class IndexedDbStamper { constructor() { if (typeof window === "undefined") { throw new Error("IndexedDB is only available in the browser"); } } async openDb() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, 1); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(DB_STORE)) { db.createObjectStore(DB_STORE); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async storeKeyPair(publicKeyHex, privateKey) { const db = await this.openDb(); return new Promise((resolve, reject) => { const tx = db.transaction(DB_STORE, "readwrite"); const store = tx.objectStore(DB_STORE); store.put(privateKey, publicKeyHex); tx.oncomplete = () => { db.close(); resolve(); }; tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error); }); } async getPrivateKey(publicKeyHex) { const db = await this.openDb(); return new Promise((resolve, reject) => { const tx = db.transaction(DB_STORE, "readonly"); const store = tx.objectStore(DB_STORE); const request = store.get(publicKeyHex); request.onsuccess = () => { db.close(); resolve(request.result || null); }; request.onerror = () => { db.close(); reject(request.error); }; }); } async listKeyPairs() { const db = await this.openDb(); return new Promise((resolve, reject) => { const tx = db.transaction(DB_STORE, "readonly"); const store = tx.objectStore(DB_STORE); const request = store.getAllKeys(); request.onsuccess = () => { db.close(); resolve(request.result); }; request.onerror = () => { db.close(); reject(request.error); }; }); } async createKeyPair(externalKeyPair) { let privateKey; let publicKey; if (externalKeyPair) { await utils.assertValidP256ECDSAKeyPair(externalKeyPair); privateKey = externalKeyPair.privateKey; publicKey = externalKeyPair.publicKey; } else { const keyPair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, // Non-extractable private key ["sign", "verify"]); privateKey = keyPair.privateKey; publicKey = keyPair.publicKey; } const rawPubKey = new Uint8Array(await crypto.subtle.exportKey("raw", publicKey)); const compressedPubKey = encoding.pointEncode(rawPubKey); const compressedHex = encoding.uint8ArrayToHexString(compressedPubKey); await this.storeKeyPair(compressedHex, privateKey); return compressedHex; } async deleteKeyPair(publicKeyHex) { const db = await this.openDb(); return new Promise((resolve, reject) => { const tx = db.transaction(DB_STORE, "readwrite"); const store = tx.objectStore(DB_STORE); store.delete(publicKeyHex); tx.oncomplete = () => { db.close(); resolve(); }; tx.onerror = () => reject(tx.error); }); } async sign(payload, publicKeyHex, format) { const privateKey = await this.getPrivateKey(publicKeyHex); if (!privateKey) { throw new Error("Key not found for publicKey: " + publicKeyHex); } const encodedPayload = new TextEncoder().encode(payload); const signatureIeee1363 = await crypto.subtle.sign({ name: "ECDSA", hash: { name: "SHA-256" } }, privateKey, encodedPayload); if (format === apiKeyStamper.SignatureFormat.Raw) { return encoding.uint8ArrayToHexString(new Uint8Array(signatureIeee1363)); } else { return encoding.uint8ArrayToHexString(convertEcdsaIeee1363ToDer(new Uint8Array(signatureIeee1363))); } } async stamp(payload, publicKeyHex) { const signature = await this.sign(payload, publicKeyHex, apiKeyStamper.SignatureFormat.Der); const stamp = { publicKey: publicKeyHex, scheme: "SIGNATURE_SCHEME_TK_API_P256", signature, }; return { stampHeaderName, stampHeaderValue: encoding.stringToBase64urlString(JSON.stringify(stamp)), }; } } exports.IndexedDbStamper = IndexedDbStamper; //# sourceMappingURL=stamper.js.map