@turnkey/core
Version:
A core JavaScript web and React Native package for interfacing with Turnkey's infrastructure.
118 lines (115 loc) • 4.91 kB
JavaScript
import { ApiKeyStamper, SignatureFormat } from '@turnkey/api-key-stamper';
import { generateP256KeyPair } from '@turnkey/crypto';
let Keychain;
try {
Keychain = require("react-native-keychain");
}
catch {
throw new Error("Please install react-native-keychain in your app to use ReactNativeKeychainStamper");
}
// In versions <=1.8.0, keys were stored using just the publicKey as the keychain service name
// This caused `listKeyPairs()` to return ALL keychain entries (including non-Turnkey ones),
// which meant `clearUnusedKeyPairs()` would delete the user's own keychain data
//
// To fix this, we now prefix all Turnkey-managed keys with this constant to scope them
// properly to Turnkey. Methods that read or delete keys still fall back to the unprefixed
// service name to migrate legacy keys. This fallback can be removed once we're confident
// all users have migrated to the prefixed format
const TURNKEY_KEY_PREFIX = "com.turnkey.keypair:";
class ReactNativeKeychainStamper {
serviceName(publicKeyHex) {
return `${TURNKEY_KEY_PREFIX}${publicKeyHex}`;
}
async listKeyPairs() {
const allServices = await Keychain.getAllGenericPasswordServices();
return allServices
.filter((service) => service.startsWith(TURNKEY_KEY_PREFIX))
.map((service) => service.slice(TURNKEY_KEY_PREFIX.length));
}
async createKeyPair(externalKeyPair) {
let privateKey;
let publicKey;
if (externalKeyPair) {
privateKey = externalKeyPair.privateKey;
publicKey = externalKeyPair.publicKey;
}
else {
const pair = generateP256KeyPair();
privateKey = pair.privateKey;
publicKey = pair.publicKey;
}
// we store in Keychain with a
// Turnkey-specific service prefix
await Keychain.setGenericPassword(publicKey, privateKey, {
service: this.serviceName(publicKey),
});
return publicKey;
}
async deleteKeyPair(publicKeyHex) {
// we check if the key exists under the prefixed service name
// - if it exists, we delete that
// - otherwise, we assume it's a legacy (unprefixed) key and try to delete that
const hasPrefixed = await Keychain.getGenericPassword({
service: this.serviceName(publicKeyHex),
});
await Keychain.resetGenericPassword({
service: hasPrefixed ? this.serviceName(publicKeyHex) : publicKeyHex,
});
}
async getPrivateKey(publicKeyHex) {
// we check if the key exists under the prefixed service name
// - if it exists, we return the private key
// - otherwise, we assume it's a legacy (unprefixed) key, migrate it
// to the prefixed format, and return the private key
const prefixedCreds = await Keychain.getGenericPassword({
service: this.serviceName(publicKeyHex),
});
if (prefixedCreds)
return prefixedCreds.password;
// we fall back to the unprefixed (legacy) service name
const creds = await Keychain.getGenericPassword({
service: publicKeyHex,
});
if (!creds)
return null;
// migrate the legacy key to the prefixed format so it's properly scoped going forward
await Keychain.setGenericPassword(creds.username, creds.password, {
service: this.serviceName(publicKeyHex),
});
await Keychain.resetGenericPassword({ service: publicKeyHex });
return creds.password;
}
async stamp(payload, publicKeyHex) {
const privateKey = await this.getPrivateKey(publicKeyHex);
if (!privateKey) {
throw new Error(`No private key found for public key: ${publicKeyHex}`);
}
const stamper = new ApiKeyStamper({
apiPublicKey: publicKeyHex,
apiPrivateKey: privateKey,
});
const { stampHeaderName, stampHeaderValue } = await stamper.stamp(payload);
return { stampHeaderName, stampHeaderValue };
}
async sign(payload, publicKeyHex, format) {
const privateKey = await this.getPrivateKey(publicKeyHex);
if (!privateKey) {
throw new Error(`No private key found for public key: ${publicKeyHex}`);
}
const stamper = new ApiKeyStamper({
apiPublicKey: publicKeyHex,
apiPrivateKey: privateKey,
});
switch (format) {
case SignatureFormat.Raw: {
return stamper.sign(payload, SignatureFormat.Raw);
}
case SignatureFormat.Der:
return stamper.sign(payload, SignatureFormat.Der);
default:
throw new Error(`Unsupported signature format: ${format}`);
}
}
}
export { ReactNativeKeychainStamper };
//# sourceMappingURL=stamper.mjs.map