├── .npmignore ├── src ├── sharedSecretCache.ts ├── index.ts ├── asArray.ts ├── generateKeypair.ts ├── deriveKeyWithCache.ts ├── BRC43Helpers.ts ├── getPaymentPrivateKey.ts ├── deriveKey.ts └── getPaymentAddress.ts ├── .gitignore ├── .eslintrc.json ├── tsconfig.json ├── jest.config.ts ├── test ├── getPaymentAddress.vectorGenerator.js ├── getPaymentPrivateKey.vectorGenerator.js ├── generateKeypair.test.js ├── getPaymentPrivateKey.vectors.js ├── getPaymentAddress.vectors.js ├── sendover.test.js ├── getPaymentAddress.test.js ├── getPaymentAddressTs.test.ts ├── getPaymentPrivateKey.test.js └── deriveKey.test.ts ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | test/ -------------------------------------------------------------------------------- /src/sharedSecretCache.ts: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | tsconfig.tsbuildinfo 4 | *.tgz 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generateKeypair' 2 | export * from './getPaymentAddress' 3 | export * from './getPaymentPrivateKey' 4 | export * from './deriveKey' 5 | export * from './deriveKeyWithCache' 6 | export * from './BRC43Helpers' 7 | export * from './asArray' 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "overrides": [ 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": [ 18 | "@typescript-eslint" 19 | ], 20 | "rules": { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "outDir": "out", 8 | "allowJs": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "noImplicitAny": false, 14 | "declaration": true, 15 | "declarationMap": true, 16 | "rootDir": ".", 17 | "composite": true, 18 | "noImplicitOverride": true 19 | }, 20 | "include": ["src/**/*"], 21 | "exclude": ["node_modules", "jest.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /src/asArray.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Coerce a value to number[] 3 | * @param val Buffer or string or number[]. If string, encoding param applies. 4 | * @param encoding defaults to 'hex' 5 | * @returns input val if it is a number[]; if string converts to Buffer using encoding; uses Array.from to convert buffer to number[] 6 | * @publicbody 7 | */ 8 | export function asArray(val: Buffer | string | number[], encoding?: BufferEncoding): number[] { 9 | let a: number[] 10 | if (Array.isArray(val)) a = val 11 | else if (Buffer.isBuffer(val)) a = Array.from(val) 12 | else a = Array.from(Buffer.from(val, encoding || 'hex')) 13 | return a 14 | } 15 | 16 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | //import { defaults } from 'jest-config' 3 | 4 | export default async (): Promise => { 5 | //console.log(defaults) 6 | return { 7 | bail: 1, 8 | verbose: true, 9 | // default is '.' 10 | rootDir: '.', 11 | // Must include source and test folders: default is [''] 12 | roots: [""], 13 | // Speed up by restricting to module (source files) extensions used. 14 | moduleFileExtensions: ['ts', 'js'], 15 | // excluded source files... 16 | modulePathIgnorePatterns: ['out/src', 'out/test'], 17 | // Default is 'node' 18 | testEnvironment: 'node', 19 | // default [ '**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)' ] 20 | testMatch: [ '**/?(*.)+(test).[tj]s' ], 21 | // default [] 22 | testRegex: [], 23 | transform: { '^.+\\.ts$': ['ts-jest', { 'rootDir': "." }] }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/getPaymentAddress.vectorGenerator.js: -------------------------------------------------------------------------------- 1 | const { getPaymentAddress } = require('../out/src/getPaymentAddress') 2 | const { generateKeypair } = require('../out/src/generateKeypair') 3 | 4 | const vectors = [] 5 | for (let i = 0; i < 5; i++) { 6 | const senderKeypair = generateKeypair() 7 | const recipientKeypair = generateKeypair() 8 | const invoiceNumber = require('crypto') 9 | .randomBytes(8) 10 | .toString('base64') 11 | const result = getPaymentAddress({ 12 | senderPrivateKey: senderKeypair.privateKey, 13 | recipientPublicKey: recipientKeypair.publicKey, 14 | invoiceNumber: invoiceNumber, 15 | returnType: 'publicKey' 16 | }) 17 | vectors.push({ 18 | senderPrivateKey: senderKeypair.privateKey, 19 | recipientPublicKey: recipientKeypair.publicKey, 20 | invoiceNumber: invoiceNumber, 21 | publicKey: result 22 | }) 23 | } 24 | 25 | console.log(JSON.stringify(vectors, null, 2)) 26 | -------------------------------------------------------------------------------- /src/generateKeypair.ts: -------------------------------------------------------------------------------- 1 | import bsv from 'babbage-bsv' 2 | 3 | /** 4 | * Generates a public/private keypair for the sending and receiving of invoices. 5 | * 6 | * @param params All parameters are given in an object 7 | * @param params.returnType='hex' Return type, either "hex" or "babbage-bsv" 8 | * 9 | * @returns The generated keypair, with `privateKey` and `publicKey` properties. 10 | */ 11 | export function generateKeypair (params?: { returnType?: 'hex' | 'babbage-bsv' } 12 | ): { 13 | privateKey: string | bsv.PrivateKey 14 | publicKey: string | bsv.PublicKey 15 | } { 16 | params ||= {} 17 | params.returnType ||= 'hex' 18 | 19 | const privateKey = bsv.PrivateKey.fromRandom() 20 | 21 | if (params.returnType === 'babbage-bsv') { 22 | return { 23 | privateKey, 24 | publicKey: privateKey.publicKey 25 | } 26 | } else { 27 | return { 28 | privateKey: privateKey.bn.toHex({ size: 32 }), 29 | publicKey: privateKey.publicKey.toString() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/getPaymentPrivateKey.vectorGenerator.js: -------------------------------------------------------------------------------- 1 | const { getPaymentPrivateKey } = require('../out/src/getPaymentPrivateKey') 2 | const { generateKeypair } = require('../out/src/generateKeypair') 3 | 4 | const generateTestVectors = () => { 5 | const vectors = [] 6 | for (let i = 0; i < 5; i++) { 7 | const senderKeypair = generateKeypair() 8 | const recipientKeypair = generateKeypair() 9 | const invoiceNumber = require('crypto') 10 | .randomBytes(8) 11 | .toString('base64') 12 | const result = getPaymentPrivateKey({ 13 | senderPublicKey: senderKeypair.publicKey, 14 | recipientPrivateKey: recipientKeypair.privateKey, 15 | invoiceNumber: invoiceNumber, 16 | returnType: 'hex' 17 | }) 18 | vectors.push({ 19 | senderPublicKey: senderKeypair.publicKey, 20 | recipientPrivateKey: recipientKeypair.privateKey, 21 | invoiceNumber: invoiceNumber, 22 | privateKey: result 23 | }) 24 | } 25 | return vectors 26 | } 27 | module.exports = { generateTestVectors } 28 | -------------------------------------------------------------------------------- /test/generateKeypair.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const { generateKeypair } = require('../out/src/generateKeypair') 3 | const bsv = require('babbage-bsv') 4 | 5 | describe('generateKeypair', () => { 6 | it('Returns object with publicKey / privateKey string properties', () => { 7 | const result = generateKeypair() 8 | expect(result.constructor).toEqual(Object) 9 | expect(result).toEqual({ 10 | publicKey: expect.any(String), 11 | privateKey: expect.any(String) 12 | }) 13 | }) 14 | it('Produces a 33-byte public key', () => { 15 | const { publicKey } = generateKeypair() 16 | expect(publicKey.length).toBe(66) 17 | const regex = /[0-9a-f]{66}/ 18 | expect(regex.test(publicKey)).toEqual(true) 19 | }) 20 | it('Produces a 32-byte private key', () => { 21 | const { privateKey } = generateKeypair() 22 | expect(privateKey.length).toBe(64) 23 | const regex = /[0-9a-f]{64}/ 24 | expect(regex.test(privateKey)).toEqual(true) 25 | }) 26 | it('Produces the correct public key for the private key', () => { 27 | const { privateKey, publicKey } = generateKeypair() 28 | const testPrivateKey = bsv.PrivateKey.fromString(privateKey) 29 | const testPublicKeyString = testPrivateKey.publicKey.toString() 30 | expect(testPublicKeyString).toEqual(publicKey) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sendover", 3 | "version": "1.3.27", 4 | "description": "Tools for creating and paying invoices privately on Bitcoin SV", 5 | "main": "./out/src/index.js", 6 | "types": "./out/src/index.d.ts", 7 | "files": [ 8 | "out", 9 | "src" 10 | ], 11 | "scripts": { 12 | "test": "jest", 13 | "test:watch": "jest --watch", 14 | "lint": "ts-standard --fix src/**/*.ts", 15 | "build": "tsc -p tsconfig.json", 16 | "prepublish": "npm run build" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/p2ppsr/sendover.git" 21 | }, 22 | "keywords": [ 23 | "Bitcoin", 24 | "BSV", 25 | "invoice", 26 | "private", 27 | "address" 28 | ], 29 | "author": "Peer-to-peer Privacy Systems Research LLC", 30 | "license": "Open BSV License", 31 | "bugs": { 32 | "url": "https://github.com/p2ppsr/sendover/issues" 33 | }, 34 | "homepage": "https://github.com/p2ppsr/sendover#readme", 35 | "devDependencies": { 36 | "@types/bn.js": "^5.1.1", 37 | "@types/jest": "^29.5.11", 38 | "@types/node": "^18.7.4", 39 | "@typescript-eslint/eslint-plugin": "^5.52.0", 40 | "@typescript-eslint/parser": "^5.52.0", 41 | "eslint": "^8.34.0", 42 | "jest": "^29.4.3", 43 | "standard": "^16.0.3", 44 | "ts-jest": "^29.0.5", 45 | "ts-node": "^10.9.1", 46 | "ts-standard": "^12.0.2", 47 | "ts2md": "^0.2.4", 48 | "typescript": "^5.2.2" 49 | }, 50 | "dependencies": { 51 | "@bsv/sdk": "^1.1.23", 52 | "babbage-bsv": "^0.2.5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/getPaymentPrivateKey.vectors.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | senderPublicKey: '033f9160df035156f1c48e75eae99914fa1a1546bec19781e8eddb900200bff9d1', 4 | recipientPrivateKey: '6a1751169c111b4667a6539ee1be6b7cd9f6e9c8fe011a5f2fe31e03a15e0ede', 5 | invoiceNumber: 'f3WCaUmnN9U=', 6 | privateKey: '761656715bbfa172f8f9f58f5af95d9d0dfd69014cfdcacc9a245a10ff8893ef' 7 | }, 8 | { 9 | senderPublicKey: '027775fa43959548497eb510541ac34b01d5ee9ea768de74244a4a25f7b60fae8d', 10 | recipientPrivateKey: 'cab2500e206f31bc18a8af9d6f44f0b9a208c32d5cca2b22acfe9d1a213b2f36', 11 | invoiceNumber: '2Ska++APzEc=', 12 | privateKey: '09f2b48bd75f4da6429ac70b5dce863d5ed2b350b6f2119af5626914bdb7c276' 13 | }, 14 | { 15 | senderPublicKey: '0338d2e0d12ba645578b0955026ee7554889ae4c530bd7a3b6f688233d763e169f', 16 | recipientPrivateKey: '7a66d0896f2c4c2c9ac55670c71a9bc1bdbdfb4e8786ee5137cea1d0a05b6f20', 17 | invoiceNumber: 'cN/yQ7+k7pg=', 18 | privateKey: '7114cd9afd1eade02f76703cc976c241246a2f26f5c4b7a3a0150ecc745da9f0' 19 | }, 20 | { 21 | senderPublicKey: '02830212a32a47e68b98d477000bde08cb916f4d44ef49d47ccd4918d9aaabe9c8', 22 | recipientPrivateKey: '6e8c3da5f2fb0306a88d6bcd427cbfba0b9c7f4c930c43122a973d620ffa3036', 23 | invoiceNumber: 'm2/QAsmwaA4=', 24 | privateKey: 'f1d6fb05da1225feeddd1cf4100128afe09c3c1aadbffbd5c8bd10d329ef8f40' 25 | }, 26 | { 27 | senderPublicKey: '03f20a7e71c4b276753969e8b7e8b67e2dbafc3958d66ecba98dedc60a6615336d', 28 | recipientPrivateKey: 'e9d174eff5708a0a41b32624f9b9cc97ef08f8931ed188ee58d5390cad2bf68e', 29 | invoiceNumber: 'jgpUIjWFlVQ=', 30 | privateKey: 'c5677c533f17c30f79a40744b18085632b262c0c13d87f3848c385f1389f79a6' 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /test/getPaymentAddress.vectors.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | senderPrivateKey: '583755110a8c059de5cd81b8a04e1be884c46083ade3f779c1e022f6f89da94c', 4 | recipientPublicKey: '02c0c1e1a1f7d247827d1bcf399f0ef2deef7695c322fd91a01a91378f101b6ffc', 5 | invoiceNumber: 'IBioA4D/OaE=', 6 | publicKey: '03c1bf5baadee39721ae8c9882b3cf324f0bf3b9eb3fc1b8af8089ca7a7c2e669f' 7 | }, 8 | { 9 | senderPrivateKey: '2c378b43d887d72200639890c11d79e8f22728d032a5733ba3d7be623d1bb118', 10 | recipientPublicKey: '039a9da906ecb8ced5c87971e9c2e7c921e66ad450fd4fc0a7d569fdb5bede8e0f', 11 | invoiceNumber: 'PWYuo9PDKvI=', 12 | publicKey: '0398cdf4b56a3b2e106224ff3be5253afd5b72de735d647831be51c713c9077848' 13 | }, 14 | { 15 | senderPrivateKey: 'd5a5f70b373ce164998dff7ecd93260d7e80356d3d10abf928fb267f0a6c7be6', 16 | recipientPublicKey: '02745623f4e5de046b6ab59ce837efa1a959a8f28286ce9154a4781ec033b85029', 17 | invoiceNumber: 'X9pnS+bByrM=', 18 | publicKey: '0273eec9380c1a11c5a905e86c2d036e70cbefd8991d9a0cfca671f5e0bbea4a3c' 19 | }, 20 | { 21 | senderPrivateKey: '46cd68165fd5d12d2d6519b02feb3f4d9c083109de1bfaa2b5c4836ba717523c', 22 | recipientPublicKey: '031e18bb0bbd3162b886007c55214c3c952bb2ae6c33dd06f57d891a60976003b1', 23 | invoiceNumber: '+ktmYRHv3uQ=', 24 | publicKey: '034c5c6bf2e52e8de8b2eb75883090ed7d1db234270907f1b0d1c2de1ddee5005d' 25 | }, 26 | { 27 | senderPrivateKey: '7c98b8abd7967485cfb7437f9c56dd1e48ceb21a4085b8cdeb2a647f62012db4', 28 | recipientPublicKey: '03c8885f1e1ab4facd0f3272bb7a48b003d2e608e1619fb38b8be69336ab828f37', 29 | invoiceNumber: 'PPfDTTcl1ao=', 30 | publicKey: '03304b41cfa726096ffd9d8907fe0835f888869eda9653bca34eb7bcab870d3779' 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /src/deriveKeyWithCache.ts: -------------------------------------------------------------------------------- 1 | import { deriveKey, SendOverDeriveKeyParams } from './deriveKey' 2 | 3 | // Global cache object to store the results of deriveKey function invocations 4 | const deriveKeyCache = {} 5 | 6 | /** 7 | * Generates a unique cache key based on the input parameters of the deriveKey function. 8 | * This function serializes the parameters into a string that can be used as a key in the cache object. 9 | * 10 | * @param {SendOverDeriveKeyParams} params - The input parameters to the deriveKey function. 11 | * @return {string} A unique string identifier based on the input parameters. 12 | */ 13 | function generateCacheKey (params: SendOverDeriveKeyParams): string { 14 | const key = JSON.stringify(params, (key, value) => 15 | value instanceof Uint8Array ? Array.from(value) : value, 16 | 4 17 | ) 18 | return key 19 | } 20 | 21 | /** 22 | * Modified deriveKey function that utilizes a caching mechanism. 23 | * This function first checks if the result for the given parameters is already in the cache. 24 | * If so, it returns the cached result. Otherwise, it proceeds with the derivation and stores the result in the cache. 25 | * 26 | * @param {SendOverDeriveKeyParams} params - The input parameters for the key derivation. 27 | * @return {string} Hex string of the derived key. 28 | */ 29 | export function deriveKeyWithCache (params: SendOverDeriveKeyParams): string { 30 | // Generate a unique cache key for the current invocation parameters 31 | const cacheKey = generateCacheKey(params) 32 | 33 | // Check if the result is already cached 34 | if (cacheKey in deriveKeyCache) { 35 | return deriveKeyCache[cacheKey] 36 | } 37 | 38 | // Proceed with the original deriveKey logic to derive the key 39 | const result = deriveKey(params) 40 | 41 | // Store the result in the cache with the generated cache key 42 | deriveKeyCache[cacheKey] = result 43 | 44 | return result 45 | } 46 | -------------------------------------------------------------------------------- /test/sendover.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const sendover = require('../out/src/index') 3 | const bsv = require('babbage-bsv') 4 | 5 | describe('sendover', () => { 6 | it('Works as described in README.md', () => { 7 | // The merchant generates a keypair. 8 | // They put the public key on their website, and keep the private key secret. 9 | const merchantKeypair = sendover.generateKeypair() 10 | 11 | // The customer also generates a keypair. 12 | const customerKeypair = sendover.generateKeypair() 13 | 14 | // The customer and the merchant agree on an invoice number. 15 | // The customer knows the invoice number. 16 | const purchaseInvoiceNumber = '341-9945319' 17 | 18 | // The customer can now generate a Bitcoin addres for the payment. 19 | // After generating the address, the customer sends the payment. 20 | const paymentAddress = sendover.getPaymentAddress({ 21 | senderPrivateKey: customerKeypair.privateKey, 22 | recipientPublicKey: merchantKeypair.publicKey, 23 | invoiceNumber: purchaseInvoiceNumber 24 | }) 25 | 26 | // After making the payment, the customer sends a few things to the merchant. 27 | // - The Bitcoin transaction that contains the payment 28 | // - The invoice number they have agreed upon 29 | // - The customer's public key 30 | // - Any SPV proofs needed for the merchant to validate and accept the transaction 31 | const dataSentToMerchant = { 32 | customerPublicKey: customerKeypair.publicKey, 33 | paymentTransaction: '...', // transaction that pays money to the address 34 | invoiceNumber: purchaseInvoiceNumber, 35 | transactionSPVProofs: ['...'] // Any needed SPV proofs 36 | } 37 | 38 | // The merchant can now calculate the private key that unlocks the money. 39 | const privateKey = sendover.getPaymentPrivateKey({ 40 | senderPublicKey: dataSentToMerchant.customerPublicKey, 41 | recipientPrivateKey: merchantKeypair.privateKey, 42 | invoiceNumber: dataSentToMerchant.invoiceNumber 43 | }) 44 | 45 | // At the end, the merchant's private key should be the one that the customer sent the payment to. 46 | const merchantDerivedAddress = bsv.PrivateKey.fromWIF(privateKey) 47 | .toAddress().toString() 48 | expect(merchantDerivedAddress).toEqual(paymentAddress) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/BRC43Helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Protocol IDs are two element arrays: [level, name] 3 | * 4 | * level is an integer value of 0, 1, or 2 specifying the protocol's counterparty permissions policy. 5 | * 6 | * name is a string which must uniquely identify the protocol. 7 | * 8 | * Level 0: Open. Any app can use it to talk to anyone without permission. 9 | * Level 1: App-bound. Only apps with permission can use the protocol. They can use it in conjunction with any counterparty. 10 | * Level 2: Countarparty-bound: Only apps with permission can use the protocol. When permission is granted, it only applies to the specific counterparty being authorized. Other counterparties, even under the same protocol ID, will trigger new permission requests. 11 | * 12 | * For historical and convenience purposes, a protocol ID may be specified as just a name string 13 | * in which case it is promoted to the array [2, name]. 14 | * 15 | * Protocol names are normalized by the following rules. 16 | * All strings that normalize to the same value identify the same protocol. 17 | * 18 | * Protocol name normalization rules: 19 | * - only letters, numbers and spaces 20 | * - no multiple space " " 21 | * - all lower case when used 22 | * - maximum 280 characters 23 | * - must be at least 5 characters 24 | * - must not end with " protocol" 25 | * - leading and trailing spaces are removed 26 | * 27 | * @param {String} input The protocol to normalize 28 | * 29 | * @returns {String} The normalized protocol 30 | * @private 31 | */ 32 | export function normalizeProtocol (input): [number, string] { 33 | if (typeof input === 'undefined') { 34 | throw new Error('A protocol ID is required') 35 | } 36 | if (typeof input === 'string') { 37 | return [2, normalizeProtocolName(input)] 38 | } 39 | if (!Array.isArray(input) || input.length !== 2) { 40 | throw new Error('Protocol IDs must be strings or two element arrays') 41 | } 42 | const level = input[0] 43 | if (typeof level !== 'number' || !Number.isInteger(level) || level < 0 || level > 2) { 44 | throw new Error('Protocol level must be 0, 1, or 2') 45 | } 46 | return [level, normalizeProtocolName(input[1])] 47 | } 48 | 49 | const normalizeProtocolName = (input?: string): string => { 50 | if (typeof input === 'undefined') { 51 | throw new Error('A protocol name is required') 52 | } 53 | if (typeof input !== 'string') { 54 | throw new Error('Protocol names must be strings') 55 | } 56 | input = input.toLowerCase().trim() 57 | if (input.includes(' ')) { 58 | throw new Error( 59 | 'Protocol names cannot contain multiple consecutive spaces (" ")' 60 | ) 61 | } 62 | if (!/^[a-z0-9 ]+$/g.test(input)) { 63 | throw new Error( 64 | 'Protocol names can only contain letters, numbers and spaces' 65 | ) 66 | } 67 | if (input.endsWith(' protocol')) { 68 | throw new Error( 69 | 'No need to end your protocol name with " protocol"' 70 | ) 71 | } 72 | if (input.length > 280) { 73 | throw new Error('Protocol names must be 280 characters or less') 74 | } 75 | if (input.length < 5) { 76 | throw new Error('Protocol names must be 5 characters or more') 77 | } 78 | return input 79 | } 80 | 81 | export function getProtocolInvoiceNumber (params: { protocolID: string | [number, string], keyID: number | string }): string { 82 | const npID = normalizeProtocol(params.protocolID) 83 | return `${npID[0]}-${npID[1]}-${params.keyID}` 84 | } 85 | -------------------------------------------------------------------------------- /src/getPaymentPrivateKey.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import bsv from 'babbage-bsv' 3 | const BN = bsv.crypto.BN 4 | const Hash = bsv.crypto.Hash 5 | const N = bsv.crypto.Point.getN() 6 | import sharedSecretCache from './sharedSecretCache' 7 | 8 | /** 9 | * Returns a private key for use by the recipient, given the sender's public key, the recipient's private key and the invoice number. 10 | * 11 | * @param params All parametera ere provided in an object 12 | * @param params.recipientPrivateKey The private key of the recipient in WIF format 13 | * @param params.senderPublicKey The public key of the sender in hexadecimal DER format 14 | * @param params.invoiceNumber The invoice number that was used 15 | * @param params.revealCounterpartyLinkage=false When true, reveals the root shared secret between the two counterparties rather than performing key derivation, returning it as a hex string 16 | * @param params.revealPaymentLinkage=false When true, reveals the secret between the two counterparties used for this specific invoice number, rather than performing key derivation. Returns the linkage as a hex string 17 | * @param params.returnType=wif The incoming payment key return type, either `wif` or `hex` 18 | * 19 | * @returns The incoming payment key that can unlock the money. 20 | */ 21 | export function getPaymentPrivateKey(params: { 22 | recipientPrivateKey: string | bsv.crypto.BN | bsv.PrivateKey 23 | senderPublicKey: string | bsv.PublicKey 24 | invoiceNumber: string 25 | revealCounterpartyLinkage?: boolean 26 | revealPaymentLinkage?: boolean 27 | returnType?: 'wif' | 'hex' | 'buffer' | 'babbage-bsv' 28 | }): string | Buffer | bsv.PrivateKey { 29 | if (params.returnType === undefined) params.returnType = 'wif' 30 | 31 | // First, a shared secret is calculated based on the public and private keys. 32 | let publicKey: bsv.PublicKey, privateKey: bsv.PrivateKey 33 | let cacheKey: string 34 | 35 | if (typeof params.senderPublicKey === 'string') { 36 | cacheKey = `-${params.senderPublicKey}` 37 | publicKey = bsv.PublicKey.fromString(params.senderPublicKey) 38 | } else if (params.senderPublicKey instanceof bsv.PublicKey) { 39 | cacheKey = `-${params.senderPublicKey.toString()}` 40 | publicKey = params.senderPublicKey 41 | } else { 42 | throw new Error('Unrecognized format for senderPublicKey') 43 | } 44 | 45 | if (typeof params.recipientPrivateKey === 'string') { 46 | cacheKey = params.recipientPrivateKey + cacheKey 47 | privateKey = BN.fromHex(params.recipientPrivateKey) 48 | } else if (params.recipientPrivateKey instanceof BN) { 49 | cacheKey = params.recipientPrivateKey.toHex({ size: 32 }) + cacheKey 50 | privateKey = params.recipientPrivateKey 51 | } else if (params.recipientPrivateKey instanceof bsv.PrivateKey) { 52 | cacheKey = params.recipientPrivateKey.bn.toHex({ size: 32 }) + cacheKey 53 | privateKey = params.recipientPrivateKey.bn 54 | } else { 55 | throw new Error('Unrecognized format for recipientPrivateKey') 56 | } 57 | 58 | let sharedSecret 59 | if (sharedSecretCache[cacheKey]) { 60 | sharedSecret = sharedSecretCache[cacheKey] 61 | } else { 62 | sharedSecret = publicKey.point.mul(privateKey).toBuffer() 63 | sharedSecretCache[cacheKey] = sharedSecret 64 | } 65 | if (params.revealCounterpartyLinkage === true) { 66 | return sharedSecret.toString('hex') 67 | } 68 | 69 | // The invoice number is turned into a buffer. 70 | const invoiceNumber = Buffer.from(String(params.invoiceNumber), 'utf8') 71 | 72 | // An HMAC is calculated with the shared secret and the invoice number. 73 | const hmac = Hash.sha256hmac(invoiceNumber, sharedSecret) 74 | if (params.revealPaymentLinkage === true) { 75 | return hmac.toString('hex') 76 | } 77 | 78 | // Finally, the hmac is added to the private key, and the result is modulo N. 79 | const finalPrivateKey = privateKey.add(BN.fromBuffer(hmac)).mod(N) 80 | 81 | switch (params.returnType) { 82 | case 'wif': 83 | return new bsv.PrivateKey(finalPrivateKey).toWIF() 84 | case 'hex': 85 | return finalPrivateKey.toHex({ size: 32 }) 86 | case 'buffer': 87 | return finalPrivateKey.toBuffer({ size: 32 }) 88 | case 'babbage-bsv': 89 | return finalPrivateKey 90 | default: 91 | throw new Error('The return type must either be "wif" or "hex"') 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/getPaymentAddress.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const { getPaymentAddress } = require('../out/src/getPaymentAddress') 3 | const { generateKeypair } = require('../out/src/generateKeypair') 4 | const bsv = require('babbage-bsv') 5 | const testVectors = require('./getPaymentAddress.vectors') 6 | 7 | describe('getPaymentAddress', () => { 8 | it('Returns a valid Bitcoin SV address', () => { 9 | const senderKeypair = generateKeypair() 10 | const recipientKeypair = generateKeypair() 11 | const testInvoiceNumber = require('crypto') 12 | .randomBytes(8) 13 | .toString('base64') 14 | const result = getPaymentAddress({ 15 | senderPrivateKey: senderKeypair.privateKey, 16 | recipientPublicKey: recipientKeypair.publicKey, 17 | invoiceNumber: testInvoiceNumber 18 | }) 19 | expect(() => { 20 | bsv.Address.fromString(result) 21 | }).not.toThrow() 22 | }) 23 | it('Returns a valid public key', () => { 24 | const senderKeypair = generateKeypair() 25 | const recipientKeypair = generateKeypair() 26 | const testInvoiceNumber = require('crypto') 27 | .randomBytes(8) 28 | .toString('base64') 29 | const result = getPaymentAddress({ 30 | senderPrivateKey: senderKeypair.privateKey, 31 | recipientPublicKey: recipientKeypair.publicKey, 32 | invoiceNumber: testInvoiceNumber, 33 | returnType: 'publicKey' 34 | }) 35 | expect(() => { 36 | bsv.PublicKey.fromString(result) 37 | }).not.toThrow() 38 | }) 39 | it('Throws an error if an invalid return type is given', () => { 40 | const senderKeypair = generateKeypair() 41 | const recipientKeypair = generateKeypair() 42 | const testInvoiceNumber = require('crypto') 43 | .randomBytes(8) 44 | .toString('base64') 45 | expect(() => { 46 | getPaymentAddress({ 47 | senderPrivateKey: senderKeypair.privateKey, 48 | recipientPublicKey: recipientKeypair.publicKey, 49 | invoiceNumber: testInvoiceNumber, 50 | returnType: 'privateKey' 51 | }) 52 | }).toThrow(new Error( 53 | 'The return type must either be "address" or "publicKey"' 54 | )) 55 | }) 56 | it('Returns a different address with a different invoice number', () => { 57 | const senderKeypair = generateKeypair() 58 | const recipientKeypair = generateKeypair() 59 | const firstInvoiceNumber = require('crypto') 60 | .randomBytes(8) 61 | .toString('base64') 62 | const firstResult = getPaymentAddress({ 63 | senderPrivateKey: senderKeypair.privateKey, 64 | recipientPublicKey: recipientKeypair.publicKey, 65 | invoiceNumber: firstInvoiceNumber 66 | }) 67 | const secondInvoiceNumber = require('crypto') 68 | .randomBytes(8) 69 | .toString('base64') 70 | const secondResult = getPaymentAddress({ 71 | senderPrivateKey: senderKeypair.privateKey, 72 | recipientPublicKey: recipientKeypair.publicKey, 73 | invoiceNumber: secondInvoiceNumber 74 | }) 75 | expect(firstResult).not.toEqual(secondResult) 76 | }) 77 | it('Reveals the same counterparty linkage information across two invoice numbers', () => { 78 | const senderKeypair = generateKeypair() 79 | const recipientKeypair = generateKeypair() 80 | const firstInvoiceNumber = require('crypto') 81 | .randomBytes(8) 82 | .toString('base64') 83 | const firstResult = getPaymentAddress({ 84 | senderPrivateKey: senderKeypair.privateKey, 85 | recipientPublicKey: recipientKeypair.publicKey, 86 | invoiceNumber: firstInvoiceNumber, 87 | revealCounterpartyLinkage: true 88 | }) 89 | const secondInvoiceNumber = require('crypto') 90 | .randomBytes(8) 91 | .toString('base64') 92 | const secondResult = getPaymentAddress({ 93 | senderPrivateKey: senderKeypair.privateKey, 94 | recipientPublicKey: recipientKeypair.publicKey, 95 | invoiceNumber: secondInvoiceNumber, 96 | revealCounterpartyLinkage: true 97 | }) 98 | expect(firstResult).toEqual(secondResult) 99 | }) 100 | it('Reveals different payment linkage information across two invoice numbers', () => { 101 | const senderKeypair = generateKeypair() 102 | const recipientKeypair = generateKeypair() 103 | const firstInvoiceNumber = require('crypto') 104 | .randomBytes(8) 105 | .toString('base64') 106 | const firstResult = getPaymentAddress({ 107 | senderPrivateKey: senderKeypair.privateKey, 108 | recipientPublicKey: recipientKeypair.publicKey, 109 | invoiceNumber: firstInvoiceNumber, 110 | revealPaymentLinkage: true 111 | }) 112 | const secondInvoiceNumber = require('crypto') 113 | .randomBytes(8) 114 | .toString('base64') 115 | const secondResult = getPaymentAddress({ 116 | senderPrivateKey: senderKeypair.privateKey, 117 | recipientPublicKey: recipientKeypair.publicKey, 118 | invoiceNumber: secondInvoiceNumber, 119 | revealPaymentLinkage: true 120 | }) 121 | expect(firstResult).not.toEqual(secondResult) 122 | }) 123 | testVectors.forEach((vector, index) => { 124 | it(`Passes test vector #${index + 1}`, () => { 125 | expect(getPaymentAddress({ 126 | senderPrivateKey: vector.senderPrivateKey, 127 | recipientPublicKey: vector.recipientPublicKey, 128 | invoiceNumber: vector.invoiceNumber, 129 | returnType: 'publicKey' 130 | })).toEqual(vector.publicKey) 131 | }) 132 | }) 133 | }) 134 | -------------------------------------------------------------------------------- /test/getPaymentAddressTs.test.ts: -------------------------------------------------------------------------------- 1 | import { computePaymentContext, getPaymentAddress, getPaymentAddressString, getPaymentPubKey } from '../out/src/getPaymentAddress' 2 | import { generateKeypair } from '../out/src/generateKeypair' 3 | import bsv from 'babbage-bsv' 4 | import testVectors from './getPaymentAddress.vectors' 5 | import crypto from 'crypto' 6 | 7 | describe('getPaymentAddress', () => { 8 | it('Returns a valid Bitcoin SV address', () => { 9 | const senderKeypair = generateKeypair() 10 | const recipientKeypair = generateKeypair() 11 | const testInvoiceNumber = randomBytesBase64(8) 12 | const params = { 13 | senderPrivateKey: senderKeypair.privateKey, 14 | recipientPublicKey: recipientKeypair.publicKey, 15 | invoiceNumber: testInvoiceNumber 16 | } 17 | const result = getPaymentAddress(params) 18 | expect(() => { 19 | bsv.Address.fromString(result) 20 | }).not.toThrow() 21 | 22 | const r2 = getPaymentAddressString(params) 23 | expect(r2).toBe(result) 24 | }) 25 | it('Returns a valid public key', () => { 26 | const senderKeypair = generateKeypair() 27 | const recipientKeypair = generateKeypair() 28 | const testInvoiceNumber = randomBytesBase64(8) 29 | const params = { 30 | senderPrivateKey: senderKeypair.privateKey, 31 | recipientPublicKey: recipientKeypair.publicKey, 32 | invoiceNumber: testInvoiceNumber 33 | } 34 | const result = getPaymentAddress({ 35 | ...params, 36 | returnType: 'publicKey' 37 | }) 38 | expect(() => { 39 | bsv.PublicKey.fromString(result) 40 | }).not.toThrow() 41 | const r2 = getPaymentPubKey(params) 42 | expect(r2.toString()).toBe(result) 43 | }) 44 | it('Throws an error if an invalid return type is given', () => { 45 | const senderKeypair = generateKeypair() 46 | const recipientKeypair = generateKeypair() 47 | const testInvoiceNumber = randomBytesBase64(8) 48 | const params = { 49 | senderPrivateKey: senderKeypair.privateKey, 50 | recipientPublicKey: recipientKeypair.publicKey, 51 | invoiceNumber: testInvoiceNumber 52 | } 53 | expect(() => { 54 | getPaymentAddress({ 55 | ...params, 56 | returnType: <'publicKey'>'privateKey' 57 | }) 58 | }).toThrow(new Error( 59 | 'The return type must either be "address" or "publicKey"' 60 | )) 61 | }) 62 | it('Returns a different address with a different invoice number', () => { 63 | const senderKeypair = generateKeypair() 64 | const recipientKeypair = generateKeypair() 65 | const params1 = { 66 | senderPrivateKey: senderKeypair.privateKey, 67 | recipientPublicKey: recipientKeypair.publicKey, 68 | invoiceNumber: randomBytesBase64(8) 69 | } 70 | const params2 = { 71 | ...params1, 72 | invoiceNumber: randomBytesBase64(8) 73 | } 74 | const firstResult = getPaymentAddress(params1) 75 | const secondResult = getPaymentAddress(params2) 76 | expect(firstResult).not.toEqual(secondResult) 77 | const r1 = getPaymentAddressString(params1) 78 | const r2 = getPaymentAddressString(params2) 79 | expect(r1).toBe(firstResult) 80 | expect(r2).toBe(secondResult) 81 | expect(r1).not.toEqual(r2) 82 | }) 83 | it('Reveals the same counterparty linkage information across two invoice numbers', () => { 84 | const senderKeypair = generateKeypair() 85 | const recipientKeypair = generateKeypair() 86 | const params1 = { 87 | senderPrivateKey: senderKeypair.privateKey, 88 | recipientPublicKey: recipientKeypair.publicKey, 89 | invoiceNumber: randomBytesBase64(8) 90 | } 91 | const params2 = { 92 | ...params1, 93 | invoiceNumber: randomBytesBase64(8) 94 | } 95 | const firstResult = getPaymentAddress({ 96 | ...params1, 97 | revealCounterpartyLinkage: true 98 | }) 99 | const secondResult = getPaymentAddress({ 100 | ...params2, 101 | revealCounterpartyLinkage: true 102 | }) 103 | expect(firstResult).toEqual(secondResult) 104 | const r1 = computePaymentContext(params1) 105 | const r2 = computePaymentContext(params2) 106 | expect(asString(r1.sharedSecret)).toBe(firstResult) 107 | expect(asString(r2.sharedSecret)).toBe(secondResult) 108 | expect(asString(r1.sharedSecret)).toEqual(asString(r2.sharedSecret)) 109 | }) 110 | it('Reveals different payment linkage information across two invoice numbers', () => { 111 | const senderKeypair = generateKeypair() 112 | const recipientKeypair = generateKeypair() 113 | const params1 = { 114 | senderPrivateKey: senderKeypair.privateKey, 115 | recipientPublicKey: recipientKeypair.publicKey, 116 | invoiceNumber: randomBytesBase64(8) 117 | } 118 | const params2 = { 119 | ...params1, 120 | invoiceNumber: randomBytesBase64(8) 121 | } 122 | const firstResult = getPaymentAddress({ 123 | ...params1, 124 | revealPaymentLinkage: true 125 | }) 126 | const secondResult = getPaymentAddress({ 127 | ...params2, 128 | revealPaymentLinkage: true 129 | }) 130 | expect(firstResult).not.toEqual(secondResult) 131 | const r1 = computePaymentContext(params1) 132 | const r2 = computePaymentContext(params2) 133 | expect(asString(r1.hmac)).toBe(firstResult) 134 | expect(asString(r2.hmac)).toBe(secondResult) 135 | expect(asString(r1.hmac)).not.toEqual(asString(r2.hmac)) 136 | }) 137 | testVectors.forEach((vector, index) => { 138 | it(`Passes test vector #${index + 1}`, () => { 139 | const params = { 140 | senderPrivateKey: vector.senderPrivateKey, 141 | recipientPublicKey: vector.recipientPublicKey, 142 | invoiceNumber: vector.invoiceNumber 143 | } 144 | const rJs = getPaymentAddress(params) 145 | expect(rJs).toEqual(vector.address) 146 | const rTs = getPaymentAddressString(params) 147 | expect(rTs).toEqual(vector.address) 148 | }) 149 | }) 150 | }) 151 | 152 | function asString(val: Buffer | string | number[], encoding?: BufferEncoding): string { 153 | if (Array.isArray(val)) val = Buffer.from(val) 154 | return Buffer.isBuffer(val) ? val.toString(encoding ?? 'hex') : val 155 | } 156 | 157 | function randomBytes(count: number): Buffer { 158 | return crypto.randomBytes(count) 159 | } 160 | 161 | function randomBytesBase64(count: number): string { 162 | return randomBytes(count).toString('base64') 163 | } -------------------------------------------------------------------------------- /test/getPaymentPrivateKey.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const { getPaymentPrivateKey } = require('../out/src/getPaymentPrivateKey') 3 | const { generateKeypair } = require('../out/src/generateKeypair') 4 | const bsv = require('babbage-bsv') 5 | const testVectors = require('./getPaymentPrivateKey.vectors') 6 | // const { generateTestVectors } = require('./getPaymentPrivateKey.vectorGenerator') 7 | 8 | describe('getPaymentPrivateKey', () => { 9 | it('Returns a valid Bitcoin SV private key', () => { 10 | const senderKeypair = generateKeypair() 11 | const recipientKeypair = generateKeypair() 12 | const testInvoiceNumber = require('crypto') 13 | .randomBytes(8) 14 | .toString('base64') 15 | const result = getPaymentPrivateKey({ 16 | senderPublicKey: senderKeypair.publicKey, 17 | recipientPrivateKey: recipientKeypair.privateKey, 18 | invoiceNumber: testInvoiceNumber 19 | }) 20 | expect(() => { 21 | bsv.PrivateKey.fromWIF(result) 22 | }).not.toThrow() 23 | }) 24 | it('Returns a valid hex string', () => { 25 | const senderKeypair = generateKeypair() 26 | const recipientKeypair = generateKeypair() 27 | const testInvoiceNumber = require('crypto') 28 | .randomBytes(8) 29 | .toString('base64') 30 | const result = getPaymentPrivateKey({ 31 | senderPublicKey: senderKeypair.publicKey, 32 | recipientPrivateKey: recipientKeypair.privateKey, 33 | invoiceNumber: testInvoiceNumber, 34 | returnType: 'hex' 35 | }) 36 | expect(() => { 37 | bsv.PrivateKey.fromHex(result) 38 | }).not.toThrow() 39 | }) 40 | it('Returns a valid buffer array', () => { 41 | const senderKeypair = generateKeypair() 42 | const recipientKeypair = generateKeypair() 43 | const testInvoiceNumber = require('crypto') 44 | .randomBytes(8) 45 | .toString('base64') 46 | const result = getPaymentPrivateKey({ 47 | senderPublicKey: senderKeypair.publicKey, 48 | recipientPrivateKey: recipientKeypair.privateKey, 49 | invoiceNumber: testInvoiceNumber, 50 | returnType: 'buffer' 51 | }) 52 | expect(() => { 53 | bsv.PrivateKey.fromBuffer(result) 54 | }).not.toThrow() 55 | }) 56 | it('Throws an error if an invalid return type is given', () => { 57 | const senderKeypair = generateKeypair() 58 | const recipientKeypair = generateKeypair() 59 | const testInvoiceNumber = require('crypto') 60 | .randomBytes(8) 61 | .toString('base64') 62 | expect(() => { 63 | getPaymentPrivateKey({ 64 | senderPublicKey: senderKeypair.publicKey, 65 | recipientPrivateKey: recipientKeypair.privateKey, 66 | invoiceNumber: testInvoiceNumber, 67 | returnType: 'publicKey' 68 | }) 69 | }).toThrow(new Error( 70 | 'The return type must either be "wif" or "hex"' 71 | )) 72 | }) 73 | it('Pads private keys with zeros if their size is less than 32 bytes', () => { 74 | // Generates a private key that meets the criteria described above 75 | const result = getPaymentPrivateKey({ 76 | senderPublicKey: '040c2cb7c02421257c7cc01e95288e0167bb4982f6ed7f06843ca908a7ee987bcc5e79df21aee01631cca74ba10b92c3053016514c79434f49e952304717df9f87', 77 | recipientPrivateKey: 'a4f4ad15349c25ed3d8bf69a713a2c3099f76adeb11cf3d1c5d9abb15e00f4a0', 78 | invoiceNumber: 1, 79 | returnType: 'hex' 80 | }) 81 | expect(result.length).toEqual(64) 82 | }) 83 | it('Returns a bsv BN object that can be used to retrieve a 32 byte hex string', () => { 84 | // Generates a private key that meets the criteria described above 85 | const result = getPaymentPrivateKey({ 86 | senderPublicKey: '040c2cb7c02421257c7cc01e95288e0167bb4982f6ed7f06843ca908a7ee987bcc5e79df21aee01631cca74ba10b92c3053016514c79434f49e952304717df9f87', 87 | recipientPrivateKey: 'a4f4ad15349c25ed3d8bf69a713a2c3099f76adeb11cf3d1c5d9abb15e00f4a0', 88 | invoiceNumber: 1, 89 | returnType: 'babbage-bsv' 90 | }) 91 | expect(result.toHex({ size: 32 }).length).toEqual(64) 92 | }) 93 | it('Returns a different private key with a different invoice number', () => { 94 | const senderKeypair = generateKeypair() 95 | const recipientKeypair = generateKeypair() 96 | const firstInvoiceNumber = require('crypto') 97 | .randomBytes(8) 98 | .toString('base64') 99 | const firstResult = getPaymentPrivateKey({ 100 | senderPublicKey: senderKeypair.publicKey, 101 | recipientPrivateKey: recipientKeypair.privateKey, 102 | invoiceNumber: firstInvoiceNumber 103 | }) 104 | const secondInvoiceNumber = require('crypto') 105 | .randomBytes(8) 106 | .toString('base64') 107 | const secondResult = getPaymentPrivateKey({ 108 | senderPublicKey: senderKeypair.publicKey, 109 | recipientPrivateKey: recipientKeypair.privateKey, 110 | invoiceNumber: secondInvoiceNumber 111 | }) 112 | expect(firstResult).not.toEqual(secondResult) 113 | }) 114 | it('Reveals the same counterparty linkage information with a different invoice number', () => { 115 | const senderKeypair = generateKeypair() 116 | const recipientKeypair = generateKeypair() 117 | const firstInvoiceNumber = require('crypto') 118 | .randomBytes(8) 119 | .toString('base64') 120 | const firstResult = getPaymentPrivateKey({ 121 | senderPublicKey: senderKeypair.publicKey, 122 | recipientPrivateKey: recipientKeypair.privateKey, 123 | invoiceNumber: firstInvoiceNumber, 124 | revealCounterpartyLinkage: true 125 | }) 126 | const secondInvoiceNumber = require('crypto') 127 | .randomBytes(8) 128 | .toString('base64') 129 | const secondResult = getPaymentPrivateKey({ 130 | senderPublicKey: senderKeypair.publicKey, 131 | recipientPrivateKey: recipientKeypair.privateKey, 132 | invoiceNumber: secondInvoiceNumber, 133 | revealCounterpartyLinkage: true 134 | }) 135 | expect(firstResult).toEqual(secondResult) 136 | }) 137 | it('Reveals different payment linkage information with a different invoice number', () => { 138 | const senderKeypair = generateKeypair() 139 | const recipientKeypair = generateKeypair() 140 | const firstInvoiceNumber = require('crypto') 141 | .randomBytes(8) 142 | .toString('base64') 143 | const firstResult = getPaymentPrivateKey({ 144 | senderPublicKey: senderKeypair.publicKey, 145 | recipientPrivateKey: recipientKeypair.privateKey, 146 | invoiceNumber: firstInvoiceNumber, 147 | revealPaymentLinkage: true 148 | }) 149 | const secondInvoiceNumber = require('crypto') 150 | .randomBytes(8) 151 | .toString('base64') 152 | const secondResult = getPaymentPrivateKey({ 153 | senderPublicKey: senderKeypair.publicKey, 154 | recipientPrivateKey: recipientKeypair.privateKey, 155 | invoiceNumber: secondInvoiceNumber, 156 | revealPaymentLinkage: true 157 | }) 158 | expect(firstResult).not.toEqual(secondResult) 159 | }) 160 | // const testVectors = generateTestVectors() 161 | testVectors.forEach((vector, index) => { 162 | it(`Passes test vector #${index + 1}`, () => { 163 | const privateKey = getPaymentPrivateKey({ 164 | senderPublicKey: vector.senderPublicKey, 165 | recipientPrivateKey: vector.recipientPrivateKey, 166 | invoiceNumber: vector.invoiceNumber, 167 | returnType: 'hex' 168 | }) 169 | expect(privateKey.length).toEqual(64) 170 | expect(privateKey).toEqual(vector.privateKey) 171 | }) 172 | }) 173 | }) 174 | -------------------------------------------------------------------------------- /src/deriveKey.ts: -------------------------------------------------------------------------------- 1 | import { getPaymentPrivateKey, getPaymentAddress, normalizeProtocol, getProtocolInvoiceNumber } from '.' 2 | import bsv from 'babbage-bsv' 3 | 4 | /** 5 | * Input params to the `deriveKey` function. 6 | * 7 | * This function derives the child key given the root key. 8 | * 9 | * The flags: 10 | * 11 | * rootKey, identityKey, publicKey, and sharedSymmetricKey flags 12 | * 13 | * can be combined with: 14 | * 15 | * counterparty, protocolID and keyID 16 | * 17 | * to derive the required key. 18 | */ 19 | export interface SendOverDeriveKeyParams { 20 | /* 21 | * The root key for derivation 22 | */ 23 | key: Uint8Array 24 | /* 25 | * The counterparty to use for derivation. Can be "self", "anyone", or the public key of a counterparty. 26 | * 27 | * public key must be a babbage-bsv PublicKey object. 28 | * 29 | * Only the counterparty can derive the corresponding private keys for asymmetric operations, 30 | * or the corresponding shared symmetric key in symmetric operations. 31 | */ 32 | counterparty: string | 'self' | 'anyone' | bsv.PublicKey 33 | /* 34 | * The protocol under which this key is used. 35 | */ 36 | protocolID: string | [number, string] 37 | /* 38 | * The specific key to derive under this protocol. 39 | */ 40 | keyID: string 41 | /* 42 | * Optional, defaults to '1'. The identity under which key derivation should occur (default 1) 43 | */ 44 | derivationIdentity: string 45 | /* 46 | * Optional, defaults to false. Whether the root key should be returned 47 | */ 48 | rootKey?: boolean 49 | /* 50 | * Optional, defaults to false. Whether the identity key should be returned, only works if rootKey = false 51 | */ 52 | identityKey?: boolean 53 | /* 54 | * Optional, defaults to false. Whether a public key should be derived 55 | */ 56 | publicKey?: boolean 57 | /* 58 | * Optional, defaults to false. Whether the derived key corresponds to a private key held by the current user. 59 | */ 60 | forSelf?: boolean 61 | /* 62 | * Optional, defaults to false. Whether a shared symmetric key should be returned. Cannot be used when publicKey = true 63 | */ 64 | sharedSymmetricKey?: boolean 65 | /* 66 | * Whether to derive from the root key, rather than the provided identity (default true) 67 | */ 68 | deriveFromRoot?: boolean 69 | /** 70 | * 71 | */ 72 | revealCounterpartyLinkage?: boolean 73 | /** 74 | * Optional, defaults to false. 75 | */ 76 | revealPaymentLinkage?: boolean 77 | } 78 | 79 | /** 80 | * This function derives the child key given the root key. 81 | * 82 | * The rootKey, identityKey, publicKey, and sharedSymmetricKey flags can be combined with 83 | * counterparty, protocolID and keyID to derive the needed keys. 84 | * 85 | * @return Hex string of key to return 86 | */ 87 | export function deriveKey(params: SendOverDeriveKeyParams): string { 88 | let counterparty = params.counterparty 89 | const { 90 | key, 91 | protocolID, 92 | keyID, 93 | derivationIdentity = '1', 94 | rootKey = false, 95 | identityKey = false, 96 | publicKey = false, 97 | forSelf = false, 98 | sharedSymmetricKey = false, 99 | deriveFromRoot = true, 100 | revealCounterpartyLinkage = false, 101 | revealPaymentLinkage = false 102 | } = params 103 | 104 | if (rootKey) { 105 | if (publicKey) { 106 | return bsv.PrivateKey.fromBuffer(Buffer.from(key)) 107 | .publicKey.toString() 108 | } else { 109 | return Buffer.from(key).toString('hex') 110 | } 111 | } 112 | const rootPrivate = bsv.PrivateKey.fromBuffer(Buffer.from(key)) 113 | let identity 114 | if (deriveFromRoot) { 115 | identity = rootPrivate 116 | } else { 117 | identity = getPaymentPrivateKey({ 118 | recipientPrivateKey: rootPrivate, 119 | senderPublicKey: rootPrivate.publicKey, 120 | invoiceNumber: derivationIdentity, 121 | returnType: 'babbage-bsv' 122 | }) 123 | } 124 | if (identityKey) { 125 | if (publicKey) { 126 | return bsv.PrivateKey.fromBuffer(identity.toBuffer({ size: 32 })).publicKey.toString() 127 | } else { 128 | return identity.toHex({ size: 32 }) 129 | } 130 | } 131 | 132 | if (!counterparty) { 133 | throw new Error('counterparty must be self, anyone or a public key!') 134 | } else if (counterparty === 'self') { 135 | if (revealCounterpartyLinkage) { 136 | throw new Error('Counterparty secrets cannot be revealed for counterparty=self as specified by BRC-69') 137 | } 138 | counterparty = bsv.PrivateKey.fromBuffer(identity.toBuffer({ size: 32 })).publicKey 139 | } else if (counterparty === 'anyone') { 140 | if (sharedSymmetricKey) { 141 | throw new Error( 142 | 'Symmetric keys (such as encryption keys or HMAC keys) should not be derivable by everyone, because messages would be decryptable by anyone who knows the identity public key of the user, and HMACs would be similarly forgeable.' 143 | ) 144 | } 145 | counterparty = bsv.PrivateKey.fromHex( 146 | '0000000000000000000000000000000000000000000000000000000000000001' 147 | ).publicKey 148 | } 149 | 150 | // If the counterparty secret is requested, it's worth making absolutely certain this is not counterparty=self, even if passed in manually 151 | // This process ensures that whatever formats the keys are in, if the derivations produce the same child keys, the counterparty secret is not evealed 152 | if (revealCounterpartyLinkage) { 153 | const self = bsv.PrivateKey.fromBuffer(identity.toBuffer({ size: 32 })).publicKey 154 | const keyDerivedBySelf = getPaymentPrivateKey({ 155 | recipientPrivateKey: identity, 156 | senderPublicKey: self, 157 | invoiceNumber: 'test', 158 | returnType: 'hex' 159 | }) 160 | const keyDerivedByCounterparty = getPaymentPrivateKey({ 161 | recipientPrivateKey: identity, 162 | senderPublicKey: counterparty, 163 | invoiceNumber: 'test', 164 | returnType: 'hex' 165 | }) 166 | if (keyDerivedBySelf === keyDerivedByCounterparty) { 167 | throw new Error('Counterparty secrets cannot be revealed for counterparty=self as specified by BRC-69') 168 | } 169 | } 170 | 171 | const normalizedProtocolID = normalizeProtocol(protocolID) 172 | const invoiceNumber = getProtocolInvoiceNumber({ protocolID: normalizedProtocolID, keyID }) 173 | 174 | let derivedPublicKey 175 | if (sharedSymmetricKey || publicKey) { 176 | if (forSelf) { 177 | const ourPrivateKey = getPaymentPrivateKey({ 178 | recipientPrivateKey: identity, 179 | senderPublicKey: counterparty, 180 | invoiceNumber, 181 | returnType: 'babbage-bsv', 182 | revealCounterpartyLinkage, 183 | revealPaymentLinkage 184 | }) 185 | if (revealCounterpartyLinkage || revealPaymentLinkage) { 186 | return ourPrivateKey 187 | } 188 | return bsv.PrivateKey.fromBuffer(ourPrivateKey.toBuffer({ size: 32 })).publicKey.toString() 189 | } else { 190 | derivedPublicKey = getPaymentAddress({ 191 | senderPrivateKey: identity, 192 | recipientPublicKey: counterparty, 193 | invoiceNumber, 194 | returnType: 'babbage-bsv', 195 | revealCounterpartyLinkage, 196 | revealPaymentLinkage 197 | }) 198 | if (revealCounterpartyLinkage || revealPaymentLinkage) { 199 | return derivedPublicKey 200 | } 201 | } 202 | } 203 | if (publicKey) { 204 | if (sharedSymmetricKey) { 205 | throw new Error('Cannot return a public key for a symmetric key!') 206 | } 207 | return derivedPublicKey.toString() 208 | } 209 | const derivedPrivateKey = getPaymentPrivateKey({ 210 | recipientPrivateKey: identity, 211 | senderPublicKey: counterparty, 212 | invoiceNumber, 213 | returnType: 'babbage-bsv', 214 | revealCounterpartyLinkage, 215 | revealPaymentLinkage 216 | }) 217 | if (revealCounterpartyLinkage || revealPaymentLinkage) { 218 | return derivedPrivateKey 219 | } 220 | if (!sharedSymmetricKey) { 221 | return derivedPrivateKey.toHex({ size: 32 }) 222 | } 223 | const sharedSecret = derivedPublicKey.point.mul( 224 | derivedPrivateKey 225 | ).toBuffer().slice(1) 226 | return sharedSecret.toString('hex') 227 | } 228 | -------------------------------------------------------------------------------- /src/getPaymentAddress.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { BigNumber, PrivateKey, PublicKey, Hash, Curve } from '@bsv/sdk' 3 | import bsvJs from 'babbage-bsv' 4 | const BN = bsvJs.crypto.BN 5 | const HashJs = bsvJs.crypto.Hash 6 | const G = bsvJs.crypto.Point.getG() 7 | import sharedSecretCache from './sharedSecretCache' 8 | import { asArray } from '.' 9 | 10 | /** 11 | * Returns a payment address for use by the sender, given the recipient's public key, the sender's private key and the invoice number. 12 | * 13 | * @param params All parameters are provided in an object 14 | * @param params.senderPrivateKey The private key of the sender in WIF format 15 | * @param params.recipientPublicKey The public key of the recipient in hexadecimal DER format 16 | * @param params.invoiceNumber The invoice number to use 17 | * @param params.revealCounterpartyLinkage=false When true, reveals the root shared secret between the two counterparties rather than performing key derivation, returning it as a hex string 18 | * @param params.revealPaymentLinkage=false When true, reveals the secret between the two counterparties used for this specific invoice number, rather than performing key derivation. Returns the linkage as a hex string 19 | * @param params.returnType=address] The destination key return type, either `address` or `publicKey` 20 | * 21 | * @returns The destination address or public key 22 | */ 23 | export function getPaymentAddress(params: { 24 | senderPrivateKey: string | bsvJs.crypto.BN | bsvJs.PrivateKey 25 | recipientPublicKey: string | bsvJs.PublicKey 26 | invoiceNumber: string 27 | revealCounterpartyLinkage?: boolean 28 | revealPaymentLinkage?: boolean 29 | returnType?: 'address' | 'publicKey' | 'babbage-bsv' 30 | }): string | bsvJs.PublicKey { 31 | // First, a shared secret is calculated based on the public and private keys. 32 | let publicKey: bsvJs.PublicKey, privateKey: bsvJs.BN 33 | let cacheKey: string 34 | if (typeof params.recipientPublicKey === 'string') { 35 | cacheKey = `-${params.recipientPublicKey}` 36 | publicKey = bsvJs.PublicKey.fromString(params.recipientPublicKey) 37 | } else if (params.recipientPublicKey instanceof bsvJs.PublicKey) { 38 | cacheKey = `-${params.recipientPublicKey.toString()}` 39 | publicKey = params.recipientPublicKey 40 | } else { 41 | throw new Error('Unrecognized format for recipientPublicKey') 42 | } 43 | if (typeof params.senderPrivateKey === 'string') { 44 | cacheKey = params.senderPrivateKey + cacheKey 45 | privateKey = BN.fromHex(params.senderPrivateKey) 46 | } else if (params.senderPrivateKey instanceof BN) { 47 | cacheKey = params.senderPrivateKey.toHex({ size: 32 }) + cacheKey 48 | privateKey = params.senderPrivateKey 49 | } else if (params.senderPrivateKey instanceof bsvJs.PrivateKey) { 50 | cacheKey = params.senderPrivateKey.bn.toHex({ size: 32 }) + cacheKey 51 | privateKey = params.senderPrivateKey.bn 52 | } else { 53 | throw new Error('Unrecognized format for senderPrivateKey') 54 | } 55 | let sharedSecret 56 | if (sharedSecretCache[cacheKey]) { 57 | sharedSecret = sharedSecretCache[cacheKey] 58 | } else { 59 | sharedSecret = publicKey.point.mul(privateKey).toBuffer() 60 | sharedSecretCache[cacheKey] = sharedSecret 61 | } 62 | if (params.revealCounterpartyLinkage === true) { 63 | return sharedSecret.toString('hex') 64 | } 65 | 66 | // The invoice number is turned into a buffer. 67 | const invoiceNumber = Buffer.from(String(params.invoiceNumber), 'utf8') 68 | 69 | // An HMAC is calculated with the shared secret and the invoice number. 70 | const hmac = HashJs.sha256hmac(invoiceNumber, sharedSecret) 71 | if (params.revealPaymentLinkage === true) { 72 | return hmac.toString('hex') 73 | } 74 | 75 | // The HMAC is multiplied by the generator point. 76 | const point = G.mul(BN.fromBuffer(hmac)) 77 | 78 | // The resulting point is added to the recipient public key. 79 | const finalPublicKey = bsvJs.PublicKey.fromPoint( 80 | publicKey.point.add(point) 81 | ) 82 | 83 | // Finally, an address is calculated with the new public key. 84 | if (params.returnType === undefined || params.returnType === 'address') { 85 | return bsvJs.Address.fromPublicKey(finalPublicKey).toString() 86 | } else if (params.returnType === 'publicKey') { 87 | return finalPublicKey.toString() 88 | } else if (params.returnType === 'babbage-bsv') { 89 | return finalPublicKey 90 | } else { 91 | throw new Error('The return type must either be "address" or "publicKey"') 92 | } 93 | } 94 | 95 | export function computePaymentContext(params: { 96 | senderPrivateKey: string | BigNumber | PrivateKey 97 | recipientPublicKey: string | PublicKey 98 | invoiceNumber: string 99 | }): { publicKey: PublicKey, sharedSecret: number[], hmac: number[] } { 100 | // First, a shared secret is calculated based on the public and private keys. 101 | let publicKey: PublicKey 102 | if (typeof params.recipientPublicKey === 'string') { 103 | publicKey = PublicKey.fromString(params.recipientPublicKey) 104 | } else if (params.recipientPublicKey instanceof PublicKey) { 105 | publicKey = params.recipientPublicKey 106 | } else { 107 | throw new Error('Unrecognized format for recipientPublicKey') 108 | } 109 | let privateKey: BigNumber 110 | if (typeof params.senderPrivateKey === 'string') { 111 | privateKey = PrivateKey.fromString(params.senderPrivateKey, 'hex') 112 | } else if (params.senderPrivateKey instanceof PrivateKey) { 113 | privateKey = params.senderPrivateKey 114 | } else if (params.senderPrivateKey instanceof BigNumber) { 115 | privateKey = params.senderPrivateKey 116 | } else { 117 | throw new Error('Unrecognized format for senderPrivateKey') 118 | } 119 | const sharedSecret = publicKey.mul(privateKey).encode(true) as number[] 120 | 121 | // The invoice number is turned into a buffer. 122 | const invoiceBuffer = asArray(String(params.invoiceNumber), 'utf8') 123 | 124 | // An HMAC is calculated with the shared secret and the invoice number. 125 | const hmac = Hash.sha256hmac(sharedSecret, invoiceBuffer) 126 | 127 | const curve = new Curve() 128 | 129 | // The HMAC is multiplied by the generator point. 130 | const point = curve.g.mul(new BigNumber(hmac)) 131 | 132 | // The resulting point is added to the recipient public key. 133 | const resultPublicKey = new PublicKey(publicKey.add(point)) 134 | 135 | return { publicKey: resultPublicKey, sharedSecret, hmac } 136 | } 137 | 138 | /** 139 | * @param params All parameters are provided in an object 140 | * @param params.senderPrivateKey The private key of the sender in WIF format 141 | * @param params.recipientPublicKey The public key of the recipient in hexadecimal DER format 142 | * @param params.invoiceNumber The invoice number to use 143 | * 144 | * @returns The destination public key 145 | */ 146 | export function getPaymentPubKey(params: { 147 | senderPrivateKey: string | BigNumber | PrivateKey 148 | recipientPublicKey: string | PublicKey 149 | invoiceNumber: string 150 | }): PublicKey { 151 | const { publicKey } = computePaymentContext(params) 152 | 153 | return publicKey 154 | } 155 | 156 | /** 157 | * @param params All parameters are provided in an object 158 | * @param params.senderPrivateKey The private key of the sender in WIF format 159 | * @param params.recipientPublicKey The public key of the recipient in hexadecimal DER format 160 | * @param params.invoiceNumber The invoice number to use 161 | * 162 | * @returns The destination public key Base58 string 163 | */ 164 | export function getPaymentPubKeyString(params: { 165 | senderPrivateKey: string | BigNumber | PrivateKey 166 | recipientPublicKey: string | PublicKey 167 | invoiceNumber: string 168 | }): string { 169 | return getPaymentPubKey(params).toString() 170 | } 171 | 172 | /** 173 | * @param params All parameters are provided in an object 174 | * @param params.senderPrivateKey The private key of the sender in WIF format 175 | * @param params.recipientPublicKey The public key of the recipient in hexadecimal DER format 176 | * @param params.invoiceNumber The invoice number to use 177 | * 178 | * @returns The destination address as Base58 string 179 | */ 180 | export function getPaymentAddressString(params: { 181 | senderPrivateKey: string | BigNumber | PrivateKey 182 | recipientPublicKey: string | PublicKey 183 | invoiceNumber: string 184 | }): string { 185 | const pubKey = getPaymentPubKey(params) 186 | return pubKey.toAddress() 187 | } 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sendover 2 | 3 | Tools for creating and paying invoices privately on Bitcoin SV 4 | 5 | The code is hosted [on GitHub](https://github.com/p2ppsr/sendover) and the package is available [through NPM](https://www.npmjs.com/package/sendover). 6 | 7 | ## Installation 8 | 9 | npm i sendover 10 | 11 | ## Example Usage 12 | 13 | ```js 14 | const sendover = require('sendover') 15 | 16 | // The merchant generates a keypair. 17 | // They put the public key on their website, and keep the private key secret. 18 | const merchantKeypair = sendover.generateKeypair() 19 | 20 | // The customer also generates a keypair. 21 | const customerKeypair = sendover.generateKeypair() 22 | 23 | // The customer and the merchant agree on an invoice number. 24 | // The customer knows the invoice number. 25 | const purchaseInvoiceNumber = '341-9945319' 26 | 27 | // The customer can now generate a Bitcoin addres for the payment. 28 | // After generating the address, the customer sends the payment. 29 | const paymentAddress = sendover.getPaymentAddress({ 30 | senderPrivateKey: customerKeypair.privateKey, 31 | recipientPublicKey: merchantKeypair.publicKey, 32 | invoiceNumber: purchaseInvoiceNumber 33 | }) 34 | 35 | // After making the payment, the customer sends a few things to the merchant. 36 | // - The Bitcoin transaction that contains the payment 37 | // - The invoice number they have agreed upon 38 | // - The customer's public key 39 | // - Any SPV proofs needed for the merchant to validate and accept the transaction 40 | const dataSentToMerchant = { 41 | customerPublicKey: customerKeypair.publicKey, 42 | paymentTransaction: '...', // transaction that pays money to the address 43 | invoiceNumber: purchaseInvoiceNumber, 44 | transactionSPVProofs: ['...'] // Any needed SPV proofs 45 | } 46 | 47 | // The merchant can now calculate the private key that unlocks the money. 48 | const privateKey = sendover.getPaymentPrivateKey({ 49 | senderPublicKey: dataSentToMerchant.customerPublicKey, 50 | recipientPrivateKey: merchantKeypair.privateKey, 51 | invoiceNumber: dataSentToMerchant.invoiceNumber 52 | }) 53 | ``` 54 | 55 | ## API 56 | 57 | 58 | 59 | Links: [API](#api), [Interfaces](#interfaces), [Functions](#functions) 60 | 61 | ### Interfaces 62 | 63 | #### Interface: SendOverDeriveKeyParams 64 | 65 | Input params to the `deriveKey` function. 66 | 67 | This function derives the child key given the root key. 68 | 69 | The flags: 70 | 71 | rootKey, identityKey, publicKey, and sharedSymmetricKey flags 72 | 73 | can be combined with: 74 | 75 | counterparty, protocolID and keyID 76 | 77 | to derive the required key. 78 | 79 | ```ts 80 | export interface SendOverDeriveKeyParams { 81 | key: Uint8Array; 82 | counterparty: string | "self" | "anyone" | bsv.PublicKey; 83 | protocolID: string | [ 84 | number, 85 | string 86 | ]; 87 | keyID: string; 88 | derivationIdentity: string; 89 | rootKey?: boolean; 90 | identityKey?: boolean; 91 | publicKey?: boolean; 92 | forSelf?: boolean; 93 | sharedSymmetricKey?: boolean; 94 | deriveFromRoot?: boolean; 95 | revealCounterpartyLinkage?: boolean; 96 | revealPaymentLinkage?: boolean; 97 | } 98 | ``` 99 | 100 |
101 | 102 | Interface SendOverDeriveKeyParams Details 103 | 104 | ##### Property revealPaymentLinkage 105 | 106 | Optional, defaults to false. 107 | 108 | ```ts 109 | revealPaymentLinkage?: boolean 110 | ``` 111 | 112 |
113 | 114 | Links: [API](#api), [Interfaces](#interfaces), [Functions](#functions) 115 | 116 | --- 117 | ### Functions 118 | 119 | | | 120 | | --- | 121 | | [asArray](#function-asarray) | 122 | | [computePaymentContext](#function-computepaymentcontext) | 123 | | [deriveKey](#function-derivekey) | 124 | | [deriveKeyWithCache](#function-derivekeywithcache) | 125 | | [generateKeypair](#function-generatekeypair) | 126 | | [getPaymentAddress](#function-getpaymentaddress) | 127 | | [getPaymentAddressString](#function-getpaymentaddressstring) | 128 | | [getPaymentPrivateKey](#function-getpaymentprivatekey) | 129 | | [getPaymentPubKey](#function-getpaymentpubkey) | 130 | | [getPaymentPubKeyString](#function-getpaymentpubkeystring) | 131 | | [getProtocolInvoiceNumber](#function-getprotocolinvoicenumber) | 132 | 133 | Links: [API](#api), [Interfaces](#interfaces), [Functions](#functions) 134 | 135 | --- 136 | 137 | #### Function: generateKeypair 138 | 139 | Generates a public/private keypair for the sending and receiving of invoices. 140 | 141 | ```ts 142 | export function generateKeypair(params?: { 143 | returnType?: "hex" | "babbage-bsv"; 144 | }): { 145 | privateKey: string | bsv.PrivateKey; 146 | publicKey: string | bsv.PublicKey; 147 | } 148 | ``` 149 | 150 |
151 | 152 | Function generateKeypair Details 153 | 154 | Returns 155 | 156 | The generated keypair, with `privateKey` and `publicKey` properties. 157 | 158 | Argument Details 159 | 160 | + **params** 161 | + All parameters are given in an object 162 | + **params.returnType** 163 | + ='hex' Return type, either "hex" or "babbage-bsv" 164 | 165 |
166 | 167 | Links: [API](#api), [Interfaces](#interfaces), [Functions](#functions) 168 | 169 | --- 170 | #### Function: getPaymentAddress 171 | 172 | Returns a payment address for use by the sender, given the recipient's public key, the sender's private key and the invoice number. 173 | 174 | ```ts 175 | export function getPaymentAddress(params: { 176 | senderPrivateKey: string | bsvJs.crypto.BN | bsvJs.PrivateKey; 177 | recipientPublicKey: string | bsvJs.PublicKey; 178 | invoiceNumber: string; 179 | revealCounterpartyLinkage?: boolean; 180 | revealPaymentLinkage?: boolean; 181 | returnType?: "address" | "publicKey" | "babbage-bsv"; 182 | }): string | bsvJs.PublicKey 183 | ``` 184 | 185 |
186 | 187 | Function getPaymentAddress Details 188 | 189 | Returns 190 | 191 | The destination address or public key 192 | 193 | Argument Details 194 | 195 | + **params** 196 | + All parameters are provided in an object 197 | + **params.senderPrivateKey** 198 | + The private key of the sender in WIF format 199 | + **params.recipientPublicKey** 200 | + The public key of the recipient in hexadecimal DER format 201 | + **params.invoiceNumber** 202 | + The invoice number to use 203 | + **params.revealCounterpartyLinkage** 204 | + =false When true, reveals the root shared secret between the two counterparties rather than performing key derivation, returning it as a hex string 205 | + **params.revealPaymentLinkage** 206 | + =false When true, reveals the secret between the two counterparties used for this specific invoice number, rather than performing key derivation. Returns the linkage as a hex string 207 | + **params.returnType** 208 | + =address] The destination key return type, either `address` or `publicKey` 209 | 210 |
211 | 212 | Links: [API](#api), [Interfaces](#interfaces), [Functions](#functions) 213 | 214 | --- 215 | #### Function: computePaymentContext 216 | 217 | ```ts 218 | export function computePaymentContext(params: { 219 | senderPrivateKey: string | BigNumber | PrivateKey; 220 | recipientPublicKey: string | PublicKey; 221 | invoiceNumber: string; 222 | }): { 223 | publicKey: PublicKey; 224 | sharedSecret: number[]; 225 | hmac: number[]; 226 | } 227 | ``` 228 | 229 | Links: [API](#api), [Interfaces](#interfaces), [Functions](#functions) 230 | 231 | --- 232 | #### Function: getPaymentPubKey 233 | 234 | ```ts 235 | export function getPaymentPubKey(params: { 236 | senderPrivateKey: string | BigNumber | PrivateKey; 237 | recipientPublicKey: string | PublicKey; 238 | invoiceNumber: string; 239 | }): PublicKey 240 | ``` 241 | 242 |
243 | 244 | Function getPaymentPubKey Details 245 | 246 | Returns 247 | 248 | The destination public key 249 | 250 | Argument Details 251 | 252 | + **params** 253 | + All parameters are provided in an object 254 | + **params.senderPrivateKey** 255 | + The private key of the sender in WIF format 256 | + **params.recipientPublicKey** 257 | + The public key of the recipient in hexadecimal DER format 258 | + **params.invoiceNumber** 259 | + The invoice number to use 260 | 261 |
262 | 263 | Links: [API](#api), [Interfaces](#interfaces), [Functions](#functions) 264 | 265 | --- 266 | #### Function: getPaymentPubKeyString 267 | 268 | ```ts 269 | export function getPaymentPubKeyString(params: { 270 | senderPrivateKey: string | BigNumber | PrivateKey; 271 | recipientPublicKey: string | PublicKey; 272 | invoiceNumber: string; 273 | }): string 274 | ``` 275 | 276 |
277 | 278 | Function getPaymentPubKeyString Details 279 | 280 | Returns 281 | 282 | The destination public key Base58 string 283 | 284 | Argument Details 285 | 286 | + **params** 287 | + All parameters are provided in an object 288 | + **params.senderPrivateKey** 289 | + The private key of the sender in WIF format 290 | + **params.recipientPublicKey** 291 | + The public key of the recipient in hexadecimal DER format 292 | + **params.invoiceNumber** 293 | + The invoice number to use 294 | 295 |
296 | 297 | Links: [API](#api), [Interfaces](#interfaces), [Functions](#functions) 298 | 299 | --- 300 | #### Function: getPaymentAddressString 301 | 302 | ```ts 303 | export function getPaymentAddressString(params: { 304 | senderPrivateKey: string | BigNumber | PrivateKey; 305 | recipientPublicKey: string | PublicKey; 306 | invoiceNumber: string; 307 | }): string 308 | ``` 309 | 310 |
311 | 312 | Function getPaymentAddressString Details 313 | 314 | Returns 315 | 316 | The destination address as Base58 string 317 | 318 | Argument Details 319 | 320 | + **params** 321 | + All parameters are provided in an object 322 | + **params.senderPrivateKey** 323 | + The private key of the sender in WIF format 324 | + **params.recipientPublicKey** 325 | + The public key of the recipient in hexadecimal DER format 326 | + **params.invoiceNumber** 327 | + The invoice number to use 328 | 329 |
330 | 331 | Links: [API](#api), [Interfaces](#interfaces), [Functions](#functions) 332 | 333 | --- 334 | #### Function: getPaymentPrivateKey 335 | 336 | Returns a private key for use by the recipient, given the sender's public key, the recipient's private key and the invoice number. 337 | 338 | ```ts 339 | export function getPaymentPrivateKey(params: { 340 | recipientPrivateKey: string | bsv.crypto.BN | bsv.PrivateKey; 341 | senderPublicKey: string | bsv.PublicKey; 342 | invoiceNumber: string; 343 | revealCounterpartyLinkage?: boolean; 344 | revealPaymentLinkage?: boolean; 345 | returnType?: "wif" | "hex" | "buffer" | "babbage-bsv"; 346 | }): string | Buffer | bsv.PrivateKey 347 | ``` 348 | 349 |
350 | 351 | Function getPaymentPrivateKey Details 352 | 353 | Returns 354 | 355 | The incoming payment key that can unlock the money. 356 | 357 | Argument Details 358 | 359 | + **params** 360 | + All parametera ere provided in an object 361 | + **params.recipientPrivateKey** 362 | + The private key of the recipient in WIF format 363 | + **params.senderPublicKey** 364 | + The public key of the sender in hexadecimal DER format 365 | + **params.invoiceNumber** 366 | + The invoice number that was used 367 | + **params.revealCounterpartyLinkage** 368 | + =false When true, reveals the root shared secret between the two counterparties rather than performing key derivation, returning it as a hex string 369 | + **params.revealPaymentLinkage** 370 | + =false When true, reveals the secret between the two counterparties used for this specific invoice number, rather than performing key derivation. Returns the linkage as a hex string 371 | + **params.returnType** 372 | + =wif The incoming payment key return type, either `wif` or `hex` 373 | 374 |
375 | 376 | Links: [API](#api), [Interfaces](#interfaces), [Functions](#functions) 377 | 378 | --- 379 | #### Function: deriveKey 380 | 381 | This function derives the child key given the root key. 382 | 383 | The rootKey, identityKey, publicKey, and sharedSymmetricKey flags can be combined with 384 | counterparty, protocolID and keyID to derive the needed keys. 385 | 386 | ```ts 387 | export function deriveKey(params: SendOverDeriveKeyParams): string 388 | ``` 389 | 390 |
391 | 392 | Function deriveKey Details 393 | 394 | Returns 395 | 396 | Hex string of key to return 397 | 398 |
399 | 400 | Links: [API](#api), [Interfaces](#interfaces), [Functions](#functions) 401 | 402 | --- 403 | #### Function: deriveKeyWithCache 404 | 405 | Modified deriveKey function that utilizes a caching mechanism. 406 | This function first checks if the result for the given parameters is already in the cache. 407 | If so, it returns the cached result. Otherwise, it proceeds with the derivation and stores the result in the cache. 408 | 409 | ```ts 410 | export function deriveKeyWithCache(params: SendOverDeriveKeyParams): string 411 | ``` 412 | 413 |
414 | 415 | Function deriveKeyWithCache Details 416 | 417 | Returns 418 | 419 | Hex string of the derived key. 420 | 421 | Argument Details 422 | 423 | + **params** 424 | + The input parameters for the key derivation. 425 | 426 |
427 | 428 | Links: [API](#api), [Interfaces](#interfaces), [Functions](#functions) 429 | 430 | --- 431 | #### Function: getProtocolInvoiceNumber 432 | 433 | ```ts 434 | export function getProtocolInvoiceNumber(params: { 435 | protocolID: string | [ 436 | number, 437 | string 438 | ]; 439 | keyID: number | string; 440 | }): string 441 | ``` 442 | 443 | Links: [API](#api), [Interfaces](#interfaces), [Functions](#functions) 444 | 445 | --- 446 | #### Function: asArray 447 | 448 | Coerce a value to number[] 449 | 450 | ```ts 451 | export function asArray(val: Buffer | string | number[], encoding?: BufferEncoding): number[] { 452 | let a: number[]; 453 | if (Array.isArray(val)) 454 | a = val; 455 | else if (Buffer.isBuffer(val)) 456 | a = Array.from(val); 457 | else 458 | a = Array.from(Buffer.from(val, encoding || "hex")); 459 | return a; 460 | } 461 | ``` 462 | 463 |
464 | 465 | Function asArray Details 466 | 467 | Returns 468 | 469 | input val if it is a number[]; if string converts to Buffer using encoding; uses Array.from to convert buffer to number[] 470 | 471 | Argument Details 472 | 473 | + **val** 474 | + Buffer or string or number[]. If string, encoding param applies. 475 | + **encoding** 476 | + defaults to 'hex' 477 | 478 |
479 | 480 | Links: [API](#api), [Interfaces](#interfaces), [Functions](#functions) 481 | 482 | --- 483 | 484 | 485 | 486 | ## Credits 487 | 488 | Credit is given to the people who have worked on making these ideas into reality. In particular, we thank Xiaohui Liu for creating the [first known implementation](https://gist.github.com/xhliu/9e267e23dd7c799039befda3ae6fa244) of private addresses using this scheme, and Dr. Craig Wright for first [describing it](https://craigwright.net/blog/bitcoin-blockchain-tech/offline-addressing). 489 | 490 | ## License 491 | 492 | The license for the code in this repository is the Open BSV License. 493 | -------------------------------------------------------------------------------- /test/deriveKey.test.ts: -------------------------------------------------------------------------------- 1 | import { deriveKey } from '../out/src/deriveKey' 2 | import { getPaymentAddress } from '../out/src/getPaymentAddress' 3 | import { getPaymentPrivateKey } from '../out/src/getPaymentPrivateKey' 4 | import bsvJs from 'babbage-bsv' 5 | import { getProtocolInvoiceNumber } from '../out/src/BRC43Helpers' 6 | 7 | // A value for the key on which derivation is to be performed 8 | const key = Uint8Array.from([ 9 | 219, 3, 2, 54, 111, 133, 169, 46, 10 | 104, 185, 102, 75, 252, 62, 30, 240, 11 | 131, 248, 10, 62, 102, 44, 184, 35, 12 | 207, 194, 4, 109, 153, 59, 23, 18 13 | ]) 14 | 15 | // A value for the counterparty's key 16 | const counterpartyPriv = '1e8226e9f542196333aa2c5da061a8f1c3e189f60493930d26be4b1d1704c27f' 17 | const counterparty = bsvJs.PrivateKey.fromHex(counterpartyPriv) 18 | .publicKey.toString() 19 | // Anyone can know this key 20 | const anyone = '0000000000000000000000000000000000000000000000000000000000000001' 21 | let params 22 | 23 | describe('deriveKey', () => { 24 | beforeEach(() => { 25 | params = { 26 | key, 27 | counterparty: 'self', 28 | protocolID: 'Hello World', 29 | keyID: '1', 30 | deriveFromRoot: false 31 | } 32 | }) 33 | it('Returns the correct root private key', () => { 34 | const returnValue = deriveKey({ ...params, rootKey: true }) 35 | expect(returnValue).toEqual(Buffer.from(key).toString('hex')) 36 | }) 37 | it('Returns the correct root public key', () => { 38 | const returnValue = deriveKey({ ...params, rootKey: true, publicKey: true }) 39 | expect(returnValue).toEqual( 40 | bsvJs.PrivateKey.fromHex(Buffer.from(key).toString('hex')) 41 | .publicKey.toString() 42 | ) 43 | }) 44 | it('Returns the correct identity private key deriving from root', () => { 45 | const returnValue = deriveKey({ 46 | ...params, 47 | identityKey: true, 48 | deriveFromRoot: true 49 | }) 50 | expect(returnValue).toEqual(Buffer.from(key).toString('hex')) 51 | }) 52 | it('Returns the correct identity public key deriving from root', () => { 53 | const returnValue = deriveKey({ 54 | ...params, 55 | identityKey: true, 56 | publicKey: true, 57 | deriveFromRoot: true 58 | }) 59 | expect(returnValue).toEqual( 60 | bsvJs.PrivateKey.fromHex(Buffer.from(key).toString('hex')) 61 | .publicKey.toString() 62 | ) 63 | }) 64 | it('Returns the correct identity private key', () => { 65 | const returnValue = deriveKey({ 66 | ...params, 67 | identityKey: true, 68 | deriveFromRoot: false 69 | }) 70 | const identity = getPaymentPrivateKey({ 71 | recipientPrivateKey: Buffer.from(key).toString('hex'), 72 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 73 | .publicKey.toString(), 74 | invoiceNumber: '1', 75 | returnType: 'hex' 76 | }) 77 | expect(returnValue).toEqual(identity) 78 | }) 79 | it('Returns the correct identity public key', () => { 80 | const returnValue = deriveKey({ 81 | ...params, 82 | identityKey: true, 83 | publicKey: true, 84 | deriveFromRoot: false 85 | }) 86 | const identity = getPaymentPrivateKey({ 87 | recipientPrivateKey: Buffer.from(key).toString('hex'), 88 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 89 | .publicKey.toString(), 90 | invoiceNumber: '1', 91 | returnType: 'hex' 92 | }) 93 | expect(returnValue).toEqual( 94 | bsvJs.PrivateKey.fromHex(identity) 95 | .publicKey.toString() 96 | ) 97 | }) 98 | it('Throws Error if counterparty is not specified', () => { 99 | expect(() => { 100 | deriveKey({ ...params, counterparty: undefined }) 101 | }).toThrow(new Error( 102 | 'counterparty must be self, anyone or a public key!' 103 | )) 104 | }) 105 | it('Throws Error if public key is requested for a symmetric key', () => { 106 | expect(() => { 107 | deriveKey({ ...params, publicKey: true, sharedSymmetricKey: true }) 108 | }).toThrow(new Error( 109 | 'Cannot return a public key for a symmetric key!' 110 | )) 111 | }) 112 | it('Throws Error if counterparty = anyone for a symmetric key', () => { 113 | expect(() => { 114 | deriveKey({ ...params, counterparty: 'anyone', sharedSymmetricKey: true }) 115 | }).toThrow(new Error( 116 | 'Symmetric keys (such as encryption keys or HMAC keys) should not be derivable by everyone, because messages would be decryptable by anyone who knows the identity public key of the user, and HMACs would be similarly forgeable.' 117 | )) 118 | }) 119 | describe('When counterparty = self', () => { 120 | beforeEach(() => { 121 | params = { ...params, counterparty: 'self' } 122 | }) 123 | it('Returns a properly-derived asymmetric public key', () => { 124 | const returnValue = deriveKey({ 125 | ...params, 126 | publicKey: true, 127 | deriveFromRoot: false 128 | }) 129 | const identity = getPaymentPrivateKey({ 130 | recipientPrivateKey: Buffer.from(key).toString('hex'), 131 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 132 | .publicKey.toString(), 133 | invoiceNumber: '1', 134 | returnType: 'hex' 135 | }) 136 | // Derive the private key from the point-of-view of the counterparty 137 | const invoiceNumber = getProtocolInvoiceNumber({ protocolID: params.protocolID, keyID: params.keyID }) 138 | const correspondingPrivateKey = getPaymentPrivateKey({ 139 | recipientPrivateKey: identity, 140 | senderPublicKey: bsvJs.PrivateKey.fromHex(identity).publicKey.toString(), 141 | invoiceNumber, 142 | returnType: 'hex' 143 | }) 144 | // The public key from the key derived by the counterparty should be identical to the one that was returned by our derivation. 145 | expect(returnValue).toEqual(bsvJs.PublicKey.fromPoint( 146 | bsvJs.PrivateKey.fromHex(correspondingPrivateKey).publicKey.point 147 | ).toString()) 148 | }) 149 | it('Returns a properly-derived asymmetric private key', () => { 150 | const returnValue = deriveKey(params) 151 | const identity = getPaymentPrivateKey({ 152 | recipientPrivateKey: Buffer.from(key).toString('hex'), 153 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 154 | .publicKey.toString(), 155 | invoiceNumber: '1', 156 | returnType: 'hex' 157 | }) 158 | // Derive our public key from the point-of-view of the counterparty 159 | const invoiceNumber = getProtocolInvoiceNumber({ protocolID: params.protocolID, keyID: params.keyID }) 160 | const correspondingPublicKey = getPaymentAddress({ 161 | senderPrivateKey: identity, 162 | recipientPublicKey: bsvJs.PrivateKey.fromHex(identity) 163 | .publicKey.toString(), 164 | invoiceNumber, 165 | returnType: 'publicKey' 166 | }) 167 | // The public key derived by the counterparty should be identical to the one from the private key that was returned by our derivation. 168 | expect(bsvJs.PublicKey.fromPoint( 169 | bsvJs.PrivateKey.fromHex(returnValue).publicKey.point 170 | ).toString()).toEqual(correspondingPublicKey) 171 | }) 172 | it('Returns a properly-derived shared symmetric key', () => { 173 | const returnValue = deriveKey({ ...params, sharedSymmetricKey: true }) 174 | const identity = getPaymentPrivateKey({ 175 | recipientPrivateKey: Buffer.from(key).toString('hex'), 176 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 177 | .publicKey.toString(), 178 | invoiceNumber: '1', 179 | returnType: 'hex' 180 | }) 181 | // Derive our public key from the point-of-view of the counterparty 182 | const invoiceNumber = getProtocolInvoiceNumber({ protocolID: params.protocolID, keyID: params.keyID }) 183 | const correspondingPublicKey = getPaymentAddress({ 184 | senderPrivateKey: identity, 185 | recipientPublicKey: bsvJs.PrivateKey.fromHex(identity) 186 | .publicKey.toString(), 187 | invoiceNumber, 188 | returnType: 'publicKey' 189 | }) 190 | // Derive the private key from the point-of-view of the counterparty 191 | const correspondingPrivateKey = getPaymentPrivateKey({ 192 | recipientPrivateKey: identity, 193 | senderPublicKey: bsvJs.PrivateKey.fromHex(identity).publicKey.toString(), 194 | invoiceNumber, 195 | returnType: 'hex' 196 | }) 197 | const sharedSecret = bsvJs.PublicKey.fromString(correspondingPublicKey).point.mul( 198 | bsvJs.crypto.BN.fromHex(correspondingPrivateKey, { size: 32 }) 199 | ).toBuffer().slice(1).toString('hex') 200 | // The symmetric shared secret calculated by the counterparty should be identical to the one that was returned. 201 | expect(returnValue).toEqual(sharedSecret) 202 | }) 203 | it('Throws Error if counterparty secret revelation requested', () => { 204 | expect(() => { 205 | deriveKey({ ...params, revealCounterpartyLinkage: true }) 206 | }).toThrow(new Error( 207 | 'Counterparty secrets cannot be revealed for counterparty=self as specified by BRC-69' 208 | )) 209 | }) 210 | it('Throws Error if counterparty secret revelation requested, even if self public key is provided manually', () => { 211 | const identity = getPaymentPrivateKey({ 212 | recipientPrivateKey: Buffer.from(key).toString('hex'), 213 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 214 | .publicKey.toString(), 215 | invoiceNumber: '1', 216 | returnType: 'hex' 217 | }) 218 | const identityPublicKey = bsvJs.PrivateKey.fromHex(identity) 219 | .publicKey.toString() 220 | expect(() => { 221 | deriveKey({ 222 | ...params, 223 | counterparty: identityPublicKey, 224 | revealCounterpartyLinkage: true 225 | }) 226 | }).toThrow(new Error( 227 | 'Counterparty secrets cannot be revealed for counterparty=self as specified by BRC-69' 228 | )) 229 | }) 230 | it('Reveals BRC-69 linkage for a specific key', () => { 231 | const returnValue = deriveKey({ 232 | ...params, 233 | revealPaymentLinkage: true 234 | }) 235 | const identity = getPaymentPrivateKey({ 236 | recipientPrivateKey: Buffer.from(key).toString('hex'), 237 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 238 | .publicKey.toString(), 239 | invoiceNumber: '1', 240 | returnType: 'hex' 241 | }) 242 | // Derive our public key from the point-of-view of the counterparty 243 | const invoiceNumber = getProtocolInvoiceNumber({ protocolID: params.protocolID, keyID: params.keyID }) 244 | const linkage = getPaymentAddress({ 245 | senderPrivateKey: identity, 246 | recipientPublicKey: bsvJs.PrivateKey.fromHex(identity) 247 | .publicKey.toString(), 248 | invoiceNumber, 249 | revealPaymentLinkage: true 250 | }) 251 | // The linkage should match that derived 252 | expect(returnValue).toEqual(linkage) 253 | }) 254 | }) 255 | describe('When counterparty = anyone', () => { 256 | beforeEach(() => { 257 | params = { ...params, counterparty: 'anyone' } 258 | }) 259 | it('Returns a properly-derived asymmetric public key', () => { 260 | const returnValue = deriveKey({ 261 | ...params, 262 | publicKey: true, 263 | deriveFromRoot: false 264 | }) 265 | const identity = getPaymentPrivateKey({ 266 | recipientPrivateKey: Buffer.from(key).toString('hex'), 267 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 268 | .publicKey.toString(), 269 | invoiceNumber: '1', 270 | returnType: 'hex' 271 | }) 272 | // Derive the private key from the point-of-view of the counterparty 273 | const invoiceNumber = getProtocolInvoiceNumber({ protocolID: params.protocolID, keyID: params.keyID }) 274 | const correspondingPrivateKey = getPaymentPrivateKey({ 275 | recipientPrivateKey: anyone, 276 | senderPublicKey: bsvJs.PrivateKey.fromHex(identity).publicKey.toString(), 277 | invoiceNumber, 278 | returnType: 'hex' 279 | }) 280 | // The public key from the key derived by the counterparty should be identical to the one that was returned by our derivation. 281 | expect(returnValue).toEqual(bsvJs.PublicKey.fromPoint( 282 | bsvJs.PrivateKey.fromHex(correspondingPrivateKey).publicKey.point 283 | ).toString()) 284 | }) 285 | it('Returns a properly-derived asymmetric private key', () => { 286 | const returnValue = deriveKey(params) 287 | const invoiceNumber = getProtocolInvoiceNumber({ protocolID: params.protocolID, keyID: 1 }) 288 | const identity = getPaymentPrivateKey({ 289 | recipientPrivateKey: Buffer.from(key).toString('hex'), 290 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 291 | .publicKey.toString(), 292 | invoiceNumber: '1', 293 | returnType: 'hex' 294 | }) 295 | // Derive our public key from the point-of-view of the counterparty 296 | const correspondingPublicKey = getPaymentAddress({ 297 | senderPrivateKey: anyone, 298 | recipientPublicKey: bsvJs.PrivateKey.fromHex(identity) 299 | .publicKey.toString(), 300 | invoiceNumber, 301 | returnType: 'publicKey' 302 | }) 303 | // The public key derived by the counterparty should be identical to the one from the private key that was returned by our derivation. 304 | expect(bsvJs.PublicKey.fromPoint( 305 | bsvJs.PrivateKey.fromHex(returnValue).publicKey.point 306 | ).toString()).toEqual(correspondingPublicKey) 307 | }) 308 | }) 309 | describe('When counterparty = a foreign public key', () => { 310 | beforeEach(() => { 311 | params = { ...params, counterparty } 312 | }) 313 | it('Returns a properly-derived asymmetric public key', () => { 314 | const returnValue = deriveKey({ 315 | ...params, 316 | publicKey: true, 317 | deriveFromRoot: false 318 | }) 319 | const invoiceNumber = getProtocolInvoiceNumber({ protocolID: [2, 'hello world'], keyID: 1 }) 320 | const identity = getPaymentPrivateKey({ 321 | recipientPrivateKey: Buffer.from(key).toString('hex'), 322 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 323 | .publicKey.toString(), 324 | invoiceNumber: '1', 325 | returnType: 'hex' 326 | }) 327 | // Derive the private key from the point-of-view of the counterparty 328 | const correspondingPrivateKey = getPaymentPrivateKey({ 329 | recipientPrivateKey: counterpartyPriv, 330 | senderPublicKey: bsvJs.PrivateKey.fromHex(identity).publicKey.toString(), 331 | invoiceNumber, // 'hello world-1', 332 | returnType: 'hex' 333 | }) 334 | // The public key from the key derived by the counterparty should be identical to the one that was returned by our derivation. 335 | expect(returnValue).toEqual(bsvJs.PublicKey.fromPoint( 336 | bsvJs.PrivateKey.fromHex(correspondingPrivateKey).publicKey.point 337 | ).toString()) 338 | }) 339 | it('Returns a properly-derived asymmetric public key of our own', () => { 340 | const returnValue = deriveKey({ 341 | ...params, 342 | publicKey: true, 343 | forSelf: true, 344 | deriveFromRoot: false 345 | }) 346 | const invoiceNumber = getProtocolInvoiceNumber({ protocolID: [2, 'hello world'], keyID: 1 }) 347 | const identity = getPaymentPrivateKey({ 348 | recipientPrivateKey: Buffer.from(key).toString('hex'), 349 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 350 | .publicKey.toString(), 351 | invoiceNumber: '1', 352 | returnType: 'hex' 353 | }) 354 | // Derive our public key from the point-of-view of the counterparty 355 | const correspondingPublicKey = getPaymentAddress({ 356 | senderPrivateKey: counterpartyPriv, 357 | recipientPublicKey: bsvJs.PrivateKey.fromHex(identity).publicKey.toString(), 358 | invoiceNumber, // 'hello world-1', 359 | returnType: 'publicKey' 360 | }) 361 | // The public key that the counterparty derived should be identical to the one that was returned by our derivation. 362 | expect(returnValue).toEqual(correspondingPublicKey) 363 | }) 364 | it('Returns a properly-derived asymmetric private key', () => { 365 | const returnValue = deriveKey(params) 366 | const invoiceNumber = getProtocolInvoiceNumber({ protocolID: [2, 'hello world'], keyID: 1 }) 367 | const identity = getPaymentPrivateKey({ 368 | recipientPrivateKey: Buffer.from(key).toString('hex'), 369 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 370 | .publicKey.toString(), 371 | invoiceNumber: '1', 372 | returnType: 'hex' 373 | }) 374 | // Derive our public key from the point-of-view of the counterparty 375 | const correspondingPublicKey = getPaymentAddress({ 376 | senderPrivateKey: counterpartyPriv, 377 | recipientPublicKey: bsvJs.PrivateKey.fromHex(identity) 378 | .publicKey.toString(), 379 | invoiceNumber, // : 'hello world-1', 380 | returnType: 'publicKey' 381 | }) 382 | // The public key derived by the counterparty should be identical to the one from the private key that was returned by our derivation. 383 | expect(bsvJs.PublicKey.fromPoint( 384 | bsvJs.PrivateKey.fromHex(returnValue).publicKey.point 385 | ).toString()).toEqual(correspondingPublicKey) 386 | }) 387 | it('Returns a properly-derived shared symmetric key', () => { 388 | const returnValue = deriveKey({ ...params, sharedSymmetricKey: true }) 389 | const invoiceNumber = getProtocolInvoiceNumber({ protocolID: [2, 'hello world'], keyID: 1 }) 390 | const identity = getPaymentPrivateKey({ 391 | recipientPrivateKey: Buffer.from(key).toString('hex'), 392 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 393 | .publicKey.toString(), 394 | invoiceNumber: '1', 395 | returnType: 'hex' 396 | }) 397 | // Derive our public key from the point-of-view of the counterparty 398 | const correspondingPublicKey = getPaymentAddress({ 399 | senderPrivateKey: counterpartyPriv, 400 | recipientPublicKey: bsvJs.PrivateKey.fromHex(identity) 401 | .publicKey.toString(), 402 | invoiceNumber, // 'hello world-1', 403 | returnType: 'publicKey' 404 | }) 405 | // Derive the private key from the point-of-view of the counterparty 406 | const correspondingPrivateKey = getPaymentPrivateKey({ 407 | recipientPrivateKey: counterpartyPriv, 408 | senderPublicKey: bsvJs.PrivateKey.fromHex(identity).publicKey.toString(), 409 | invoiceNumber, // 'hello world-1', 410 | returnType: 'hex' 411 | }) 412 | const sharedSecret = bsvJs.PublicKey.fromString(correspondingPublicKey).point.mul( 413 | bsvJs.crypto.BN.fromHex(correspondingPrivateKey, { size: 32 }) 414 | ).toBuffer().slice(1).toString('hex') 415 | // The symmetric shared secret calculated by the counterparty should be identical to the one that was returned. 416 | expect(returnValue).toEqual(sharedSecret) 417 | }) 418 | it('Returns a properly-derived shared symmetric key for a custom identity', () => { 419 | const returnValue = deriveKey({ ...params, sharedSymmetricKey: true, derivationIdentity: 'custom' }) 420 | const invoiceNumber = getProtocolInvoiceNumber({ protocolID: [2, 'hello world'], keyID: 1 }) 421 | const identity = getPaymentPrivateKey({ 422 | recipientPrivateKey: Buffer.from(key).toString('hex'), 423 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 424 | .publicKey.toString(), 425 | invoiceNumber: 'custom', 426 | returnType: 'hex' 427 | }) 428 | // Derive our public key from the point-of-view of the counterparty 429 | const correspondingPublicKey = getPaymentAddress({ 430 | senderPrivateKey: counterpartyPriv, 431 | recipientPublicKey: bsvJs.PrivateKey.fromHex(identity) 432 | .publicKey.toString(), 433 | invoiceNumber, // 'hello world-1', 434 | returnType: 'publicKey' 435 | }) 436 | // Derive the private key from the point-of-view of the counterparty 437 | const correspondingPrivateKey = getPaymentPrivateKey({ 438 | recipientPrivateKey: counterpartyPriv, 439 | senderPublicKey: bsvJs.PrivateKey.fromHex(identity).publicKey.toString(), 440 | invoiceNumber, // 'hello world-1', 441 | returnType: 'hex' 442 | }) 443 | const sharedSecret = bsvJs.PublicKey.fromString(correspondingPublicKey).point.mul( 444 | bsvJs.crypto.BN.fromHex(correspondingPrivateKey, { size: 32 }) 445 | ).toBuffer().slice(1).toString('hex') 446 | // The symmetric shared secret calculated by the counterparty should be identical to the one that was returned. 447 | expect(returnValue).toEqual(sharedSecret) 448 | }) 449 | it('Reveals BRC-69 linkage for the counterparty', () => { 450 | const returnValue = deriveKey({ 451 | ...params, 452 | revealCounterpartyLinkage: true 453 | }) 454 | const identity = getPaymentPrivateKey({ 455 | recipientPrivateKey: Buffer.from(key).toString('hex'), 456 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 457 | .publicKey.toString(), 458 | invoiceNumber: '1', 459 | returnType: 'hex' 460 | }) 461 | // Derive our public key from the point-of-view of the counterparty 462 | const invoiceNumber = getProtocolInvoiceNumber({ protocolID: params.protocolID, keyID: params.keyID }) 463 | const linkage = getPaymentAddress({ 464 | senderPrivateKey: identity, 465 | recipientPublicKey: counterparty, 466 | invoiceNumber, 467 | revealCounterpartyLinkage: true 468 | }) 469 | // The linkage should match that derived 470 | expect(returnValue).toEqual(linkage) 471 | }) 472 | it('Reveals BRC-69 linkage for the specific key', () => { 473 | const returnValue = deriveKey({ 474 | ...params, 475 | revealPaymentLinkage: true 476 | }) 477 | const identity = getPaymentPrivateKey({ 478 | recipientPrivateKey: Buffer.from(key).toString('hex'), 479 | senderPublicKey: bsvJs.PrivateKey.fromBuffer(Buffer.from(key)) 480 | .publicKey.toString(), 481 | invoiceNumber: '1', 482 | returnType: 'hex' 483 | }) 484 | // Derive our public key from the point-of-view of the counterparty 485 | const invoiceNumber = getProtocolInvoiceNumber({ protocolID: params.protocolID, keyID: params.keyID }) 486 | const linkage = getPaymentAddress({ 487 | senderPrivateKey: identity, 488 | recipientPublicKey: counterparty, 489 | invoiceNumber, 490 | revealPaymentLinkage: true 491 | }) 492 | // The linkage should match that derived 493 | expect(returnValue).toEqual(linkage) 494 | }) 495 | }) 496 | }) 497 | --------------------------------------------------------------------------------