@turnkey/core
Version:
A core JavaScript web and React Native package for interfacing with Turnkey's infrastructure.
203 lines (199 loc) • 7.53 kB
JavaScript
;
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