UNPKG

@bsv/sdk

Version:

BSV Blockchain Software Development Kit

343 lines (321 loc) 12.9 kB
import { PrivateKey, PublicKey, SymmetricKey, Hash, Utils, Point } from '../primitives/index.js' import { WalletProtocol, PubKeyHex } from './Wallet.interfaces.js' export type Counterparty = PublicKey | PubKeyHex | 'self' | 'anyone' export interface KeyDeriverApi { /** * The root key from which all other keys are derived. */ rootKey: PrivateKey /** * The identity of this key deriver which is normally the public key associated with the `rootKey` */ identityKey: string /** * 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] - Optional. false if undefined. Whether deriving for self. * @returns {PublicKey} - The derived public key. */ derivePublicKey: ( protocolID: WalletProtocol, keyID: string, counterparty: Counterparty, forSelf?: boolean ) => PublicKey /** * 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: WalletProtocol, keyID: string, counterparty: Counterparty ) => PrivateKey /** * 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: WalletProtocol, keyID: string, counterparty: Counterparty ) => SymmetricKey /** * 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: Counterparty) => number[] /** * 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: Counterparty, protocolID: WalletProtocol, keyID: string ) => number[] } /** * 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 implements KeyDeriverApi { rootKey: PrivateKey identityKey: string private readonly anyone: PublicKey /** * Initializes the KeyDeriver instance with a root private key. * @param {PrivateKey | 'anyone'} rootKey - The root private key or the string 'anyone'. */ constructor ( rootKey: PrivateKey | 'anyone', private readonly cacheSharedSecret?: ((priv: PrivateKey, pub: Point, point: Point) => void), private readonly retrieveCachedSharedSecret?: ((priv: PrivateKey, pub: Point) => (Point | undefined)) ) { 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: WalletProtocol, keyID: string, counterparty: Counterparty, forSelf: boolean = false ): PublicKey { 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: WalletProtocol, keyID: string, counterparty: Counterparty ): PrivateKey { 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: WalletProtocol, keyID: string, counterparty: Counterparty ): SymmetricKey { // 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: Counterparty): number[] { 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) as number[] } /** * 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: Counterparty, protocolID: WalletProtocol, keyID: string ): number[] { 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. */ private normalizeCounterparty (counterparty: Counterparty): PublicKey { 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. */ private computeInvoiceNumber ( protocolID: WalletProtocol, keyID: string ): string { 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