@bsv/sdk
Version:
BSV Blockchain Software Development Kit
192 lines • 9.63 kB
JavaScript
import { PrivateKey, PublicKey, SymmetricKey, Hash, Utils } from '../primitives/index.js';
/**
* Class responsible for deriving various types of keys using a root private key.
* It supports deriving public and private keys, symmetric keys, and revealing key linkages.
*/
export class KeyDeriver {
cacheSharedSecret;
retrieveCachedSharedSecret;
rootKey;
identityKey;
anyone;
/**
* Initializes the KeyDeriver instance with a root private key.
* @param {PrivateKey | 'anyone'} rootKey - The root private key or the string 'anyone'.
*/
constructor(rootKey, cacheSharedSecret, retrieveCachedSharedSecret) {
this.cacheSharedSecret = cacheSharedSecret;
this.retrieveCachedSharedSecret = retrieveCachedSharedSecret;
this.anyone = new PrivateKey(1).toPublicKey();
if (rootKey === 'anyone') {
this.rootKey = new PrivateKey(1);
}
else {
this.rootKey = rootKey;
}
this.identityKey = this.rootKey.toPublicKey().toString();
}
/**
* Derives a public key based on protocol ID, key ID, and counterparty.
* @param {WalletProtocol} protocolID - The protocol ID including a security level and protocol name.
* @param {string} keyID - The key identifier.
* @param {Counterparty} counterparty - The counterparty's public key or a predefined value ('self' or 'anyone').
* @param {boolean} [forSelf=false] - Whether deriving for self.
* @returns {PublicKey} - The derived public key.
*/
derivePublicKey(protocolID, keyID, counterparty, forSelf = false) {
counterparty = this.normalizeCounterparty(counterparty);
if (forSelf) {
return this.rootKey
.deriveChild(counterparty, this.computeInvoiceNumber(protocolID, keyID), this.cacheSharedSecret, this.retrieveCachedSharedSecret)
.toPublicKey();
}
else {
return counterparty.deriveChild(this.rootKey, this.computeInvoiceNumber(protocolID, keyID), this.cacheSharedSecret, this.retrieveCachedSharedSecret);
}
}
/**
* Derives a private key based on protocol ID, key ID, and counterparty.
* @param {WalletProtocol} protocolID - The protocol ID including a security level and protocol name.
* @param {string} keyID - The key identifier.
* @param {Counterparty} counterparty - The counterparty's public key or a predefined value ('self' or 'anyone').
* @returns {PrivateKey} - The derived private key.
*/
derivePrivateKey(protocolID, keyID, counterparty) {
counterparty = this.normalizeCounterparty(counterparty);
return this.rootKey.deriveChild(counterparty, this.computeInvoiceNumber(protocolID, keyID), this.cacheSharedSecret, this.retrieveCachedSharedSecret);
}
/**
* Derives a symmetric key based on protocol ID, key ID, and counterparty.
* Note: Symmetric keys should not be derivable by everyone due to security risks.
* @param {WalletProtocol} protocolID - The protocol ID including a security level and protocol name.
* @param {string} keyID - The key identifier.
* @param {Counterparty} counterparty - The counterparty's public key or a predefined value ('self' or 'anyone').
* @returns {SymmetricKey} - The derived symmetric key.
*/
deriveSymmetricKey(protocolID, keyID, counterparty) {
// If counterparty is 'anyone', we use 1*G as the public key.
// This is a publicly derivable key and should only be used in scenarios where public disclosure is intended.
if (counterparty === 'anyone') {
counterparty = this.anyone;
}
else {
counterparty = this.normalizeCounterparty(counterparty);
}
const derivedPublicKey = this.derivePublicKey(protocolID, keyID, counterparty);
const derivedPrivateKey = this.derivePrivateKey(protocolID, keyID, counterparty);
return new SymmetricKey(derivedPrivateKey.deriveSharedSecret(derivedPublicKey)?.x?.toArray() ?? []);
}
/**
* Reveals the shared secret between the root key and the counterparty.
* Note: This should not be used for 'self'.
* @param {Counterparty} counterparty - The counterparty's public key or a predefined value ('self' or 'anyone').
* @returns {number[]} - The shared secret as a number array.
* @throws {Error} - Throws an error if attempting to reveal a shared secret for 'self'.
*/
revealCounterpartySecret(counterparty) {
if (counterparty === 'self') {
throw new Error('Counterparty secrets cannot be revealed for counterparty=self.');
}
counterparty = this.normalizeCounterparty(counterparty);
// Double-check to ensure not revealing the secret for 'self'
const self = this.rootKey.toPublicKey();
const keyDerivedBySelf = this.rootKey.deriveChild(self, 'test').toHex();
const keyDerivedByCounterparty = this.rootKey
.deriveChild(counterparty, 'test')
.toHex();
if (keyDerivedBySelf === keyDerivedByCounterparty) {
throw new Error('Counterparty secrets cannot be revealed for counterparty=self.');
}
return this.rootKey
.deriveSharedSecret(counterparty)
.encode(true);
}
/**
* Reveals the specific key association for a given protocol ID, key ID, and counterparty.
* @param {Counterparty} counterparty - The counterparty's public key or a predefined value ('self' or 'anyone').
* @param {WalletProtocol} protocolID - The protocol ID including a security level and protocol name.
* @param {string} keyID - The key identifier.
* @returns {number[]} - The specific key association as a number array.
*/
revealSpecificSecret(counterparty, protocolID, keyID) {
counterparty = this.normalizeCounterparty(counterparty);
const sharedSecret = this.rootKey.deriveSharedSecret(counterparty);
const invoiceNumberBin = Utils.toArray(this.computeInvoiceNumber(protocolID, keyID), 'utf8');
return Hash.sha256hmac(sharedSecret.encode(true), invoiceNumberBin);
}
/**
* Normalizes the counterparty to a public key.
* @param {Counterparty} counterparty - The counterparty's public key or a predefined value ('self' or 'anyone').
* @returns {PublicKey} - The normalized counterparty public key.
* @throws {Error} - Throws an error if the counterparty is invalid.
*/
normalizeCounterparty(counterparty) {
if (counterparty === null || counterparty === undefined) {
throw new Error('counterparty must be self, anyone or a public key!');
}
else if (counterparty === 'self') {
return this.rootKey.toPublicKey();
}
else if (counterparty === 'anyone') {
return new PrivateKey(1).toPublicKey();
}
else if (typeof counterparty === 'string') {
return PublicKey.fromString(counterparty);
}
else {
return counterparty;
}
}
/**
* Computes the invoice number based on the protocol ID and key ID.
* @param {WalletProtocol} protocolID - The protocol ID including a security level and protocol name.
* @param {string} keyID - The key identifier.
* @returns {string} - The computed invoice number.
* @throws {Error} - Throws an error if protocol ID or key ID are invalid.
*/
computeInvoiceNumber(protocolID, keyID) {
const securityLevel = protocolID[0];
if (!Number.isInteger(securityLevel) ||
securityLevel < 0 ||
securityLevel > 2) {
throw new Error('Protocol security level must be 0, 1, or 2');
}
const protocolName = protocolID[1].toLowerCase().trim();
if (keyID.length > 800) {
throw new Error('Key IDs must be 800 characters or less');
}
if (keyID.length < 1) {
throw new Error('Key IDs must be 1 character or more');
}
if (protocolName.length > 400) {
// Specific linkage revelation is the only protocol ID that can contain another protocol ID.
// Therefore, we allow it to be long enough to encapsulate the target protocol
if (protocolName.startsWith('specific linkage revelation ')) {
// The format is: 'specific linkage revelation x YYYYY'
// Where: x is the security level and YYYYY is the target protocol
// Thus, the max acceptable length is 30 + 400 = 430 bytes
if (protocolName.length > 430) {
throw new Error('Specific linkage revelation protocol names must be 430 characters or less');
}
}
else {
throw new Error('Protocol names must be 400 characters or less');
}
}
if (protocolName.length < 5) {
throw new Error('Protocol names must be 5 characters or more');
}
if (protocolName.includes(' ')) {
throw new Error('Protocol names cannot contain multiple consecutive spaces (" ")');
}
if (!/^[a-z0-9 ]+$/g.test(protocolName)) {
throw new Error('Protocol names can only contain letters, numbers and spaces');
}
if (protocolName.endsWith(' protocol')) {
throw new Error('No need to end your protocol name with " protocol"');
}
return `${securityLevel}-${protocolName}-${keyID}`;
}
}
export default KeyDeriver;
//# sourceMappingURL=KeyDeriver.js.map