mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-19 19:55:51 +00:00
feat: use @noble and @scure libraries for cryptography (#2273)
Switch to using `@noble/hashes`, `@noble/curves`, `@scure/base`, `@scure/bip32`, and `@scure/bip39`. This replaces `crypto` polyfills (such as `crypto-browserify`), `create-hash`, `elliptic`, `hash.js`, `bn.js` (both versions), and their many dependencies. This also means there are 33 less dependencies downloaded when running a fresh `npm install` and will make the project much easier to maintain. This reduces the bundle size by 44% (82kb minified and gzipped) over the current 3.0 branch as well as reducing the amount of configuration required to bundle. Closes #1814, #1817, #2272, and #2306 Co-authored-by: Caleb Kniffen <ckniffen@ripple.com>
This commit is contained in:
committed by
Caleb Kniffen
parent
5607320ce2
commit
217b111ef2
14
packages/isomorphic/.eslintignore
Normal file
14
packages/isomorphic/.eslintignore
Normal file
@@ -0,0 +1,14 @@
|
||||
# Don't ever lint node_modules
|
||||
node_modules
|
||||
|
||||
# Don't lint build output
|
||||
dist
|
||||
|
||||
# don't lint nyc coverage output
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Don't lint NYC configuration
|
||||
nyc.config.js
|
||||
|
||||
.idea
|
||||
75
packages/isomorphic/.eslintrc.js
Normal file
75
packages/isomorphic/.eslintrc.js
Normal file
@@ -0,0 +1,75 @@
|
||||
module.exports = {
|
||||
root: false,
|
||||
|
||||
parser: '@typescript-eslint/parser', // Make ESLint compatible with TypeScript
|
||||
parserOptions: {
|
||||
// Enable linting rules with type information from our tsconfig
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json', './tsconfig.eslint.json'],
|
||||
|
||||
sourceType: 'module', // Allow the use of imports / ES modules
|
||||
|
||||
ecmaFeatures: {
|
||||
impliedStrict: true, // Enable global strict mode
|
||||
},
|
||||
},
|
||||
|
||||
// Specify global variables that are predefined
|
||||
env: {
|
||||
browser: true, // Enable browser global variables
|
||||
node: true, // Enable node global variables & Node.js scoping
|
||||
es2020: true, // Add all ECMAScript 2020 globals and automatically set the ecmaVersion parser option to ES2020
|
||||
jest: true, // Add Jest testing global variables
|
||||
},
|
||||
|
||||
plugins: [],
|
||||
extends: ['@xrplf/eslint-config/base'],
|
||||
|
||||
rules: {
|
||||
// ** TODO **
|
||||
// all of the below are turned off for now during the migration to a
|
||||
// monorepo. They need to actually be addressed!
|
||||
// **
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-magic-numbers': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'@typescript-eslint/restrict-plus-operands': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/explicit-member-accessibility': 'off',
|
||||
'@typescript-eslint/promise-function-async': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/consistent-type-assertions': 'off',
|
||||
'import/no-unused-modules': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
'jsdoc/require-description': 'off',
|
||||
'jsdoc/require-returns': 'off',
|
||||
'jsdoc/require-description-complete-sentence': 'off',
|
||||
'jsdoc/check-tag-names': 'off',
|
||||
'jsdoc/check-examples': 'off', // Not implemented in eslint 8
|
||||
'jsdoc/no-types': 'off',
|
||||
'tsdoc/syntax': 'off',
|
||||
'import/order': 'off',
|
||||
'eslint-comments/require-description': 'off',
|
||||
'no-shadow': 'off',
|
||||
'multiline-comment-style': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
},
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ['test/*.test.ts'],
|
||||
// tests are importing through full module name to test in an isomorphic way
|
||||
rules: {
|
||||
'node/no-extraneous-import': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
12
packages/isomorphic/HISTORY.md
Normal file
12
packages/isomorphic/HISTORY.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# @xrplf/isomorphic Release History
|
||||
|
||||
## Unreleased
|
||||
|
||||
Initial release providing isomorphic and tree-shakable implementations of:
|
||||
|
||||
- ripemd160
|
||||
- sha256
|
||||
- sha512
|
||||
- bytesToHash
|
||||
- hashToBytes
|
||||
- randomBytes
|
||||
15
packages/isomorphic/LICENSE
Normal file
15
packages/isomorphic/LICENSE
Normal file
@@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2023 The XRPL developers
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
100
packages/isomorphic/README.md
Normal file
100
packages/isomorphic/README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# @xrplf/isomorphic
|
||||
|
||||
A collection of isomorphic implementations of crypto and utility functions.
|
||||
|
||||
Browser implementations of cryptographic functions use `@noble/hashes` and `crypto` for node .
|
||||
|
||||
### Hashes
|
||||
|
||||
All hash functions operate similarly to `@noble/hashes` and have the following properties:
|
||||
|
||||
- They can be called directly by providing a Uint8Array or string which will be converted into a UInt8Array via UTF-8 encoding (not hex).
|
||||
- They all return a UInt8Array.
|
||||
|
||||
```
|
||||
function hash(message: Uint8Array | string): Uint8Array;
|
||||
hash(new Uint8Array([1, 3]));
|
||||
hash('string') == hash(new TextEncoder().encode('string'));
|
||||
```
|
||||
|
||||
All hash functions can be constructed via `hash.create()` method:
|
||||
|
||||
- The result is `Hash` subclass instance, which has `update()` and `digest()` methods.
|
||||
- `digest()` finalizes the hash and makes it no longer usable
|
||||
|
||||
```typescript
|
||||
hash
|
||||
.create()
|
||||
.update(new Uint8Array([1, 3]))
|
||||
.digest();
|
||||
```
|
||||
|
||||
### `@xrplf/isomorphic/ripemd160`
|
||||
```typescript
|
||||
import { ripemd160 } from '@xrplf/isomorphic/ripemd160';
|
||||
const hashA = ripemd160('abc');
|
||||
const hashB = ripemd160
|
||||
.create()
|
||||
.update(Uint8Array.from([1, 2, 3]))
|
||||
.digest();
|
||||
```
|
||||
|
||||
### `@xrplf/isomorphic/sha256`
|
||||
|
||||
```typescript
|
||||
import { sha256 } from '@xrplf/isomorphic/sha256';
|
||||
const hashA = sha256('abc');
|
||||
const hashB = sha256
|
||||
.create()
|
||||
.update(Uint8Array.from([1, 2, 3]))
|
||||
.digest();
|
||||
```
|
||||
|
||||
### `@xrplf/isomorphic/sha512`
|
||||
|
||||
```typescript
|
||||
import { sha512 } from '@xrplf/isomorphic/sha512';
|
||||
const hashA = sha512('abc');
|
||||
const hashB = sha512
|
||||
.create()
|
||||
.update(Uint8Array.from([1, 2, 3]))
|
||||
.digest();
|
||||
```
|
||||
|
||||
## Utilities
|
||||
|
||||
### `@xrplf/isomorphic/utils`
|
||||
|
||||
#### randomBytes
|
||||
|
||||
Create an UInt8Array of the supplied size
|
||||
|
||||
```typescript
|
||||
import { randomBytes } from @xrplf/isomorphic/utils
|
||||
|
||||
console.log(randomBytes(12)) // Uint8Array(12) [95, 236, 188, 55, 208, 128, 161, 249, 171, 57, 141, 7]
|
||||
```
|
||||
|
||||
#### bytesToHex
|
||||
|
||||
Convert an UInt8Array to hex.
|
||||
|
||||
```typescript
|
||||
import { bytesToHex } from @xrplf/isomorphic/utils
|
||||
|
||||
console.log(bytesToHex([222, 173, 190, 239])) // "DEADBEEF"
|
||||
```
|
||||
|
||||
#### hexToBytes
|
||||
|
||||
Convert hex to an UInt8Array.
|
||||
|
||||
```typescript
|
||||
import { hexToBytes } from @xrplf/isomorphic/utils
|
||||
|
||||
console.log(hexToBytes('DEADBEEF')) // [222, 173, 190, 239]
|
||||
```
|
||||
|
||||
### `@xrplf/isomorphic/ws`
|
||||
|
||||
// TODO: Websocket Wrapper and `ws`
|
||||
8
packages/isomorphic/jest.config.js
Normal file
8
packages/isomorphic/jest.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Jest configuration for api
|
||||
const base = require('../../jest.config.base.js')
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
roots: [...base.roots, '<rootDir>/test'],
|
||||
displayName: '@xrplf/isomorphic',
|
||||
}
|
||||
15
packages/isomorphic/karma.config.js
Normal file
15
packages/isomorphic/karma.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const baseKarmaConfig = require('../../karma.config')
|
||||
const webpackConfig = require('./test/webpack.config')
|
||||
delete webpackConfig.entry
|
||||
|
||||
module.exports = function (config) {
|
||||
baseKarmaConfig(config)
|
||||
|
||||
config.set({
|
||||
base: '',
|
||||
webpack: webpackConfig,
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: ['test/**/*.test.ts'],
|
||||
})
|
||||
}
|
||||
44
packages/isomorphic/package.json
Normal file
44
packages/isomorphic/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@xrplf/isomorphic",
|
||||
"version": "1.0.0",
|
||||
"description": "A collection of isomorphic and tree-shakeable crypto hashes and utils for xrpl.js",
|
||||
"keywords": [
|
||||
"crypto",
|
||||
"isomorphic",
|
||||
"xrpl"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc --build ./tsconfig.build.json",
|
||||
"test": "npm run build && jest --verbose false --silent=false ./test/*.test.ts",
|
||||
"test:browser": "npm run build && karma start ./karma.config.js",
|
||||
"clean": "rm -rf ./dist ./coverage ./test/testCompiledForWeb tsconfig.build.tsbuildinfo",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"prepublish": "npm run lint && npm test"
|
||||
},
|
||||
"files": [
|
||||
"dist/*",
|
||||
"sha256/*",
|
||||
"sha512/*",
|
||||
"ripemd160/*",
|
||||
"src/*",
|
||||
"utils/*"
|
||||
],
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.18.38"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:XRPLF/xrpl.js.git"
|
||||
},
|
||||
"license": "ISC",
|
||||
"prettier": "@xrplf/prettier-config",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
7
packages/isomorphic/ripemd160/package.json
Normal file
7
packages/isomorphic/ripemd160/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@xrplf/isomorphic/ripemd160",
|
||||
"private": true,
|
||||
"main": "../dist/ripemd160",
|
||||
"types": "../dist/ripemd160",
|
||||
"browser": "../dist/ripemd160/browser.js"
|
||||
}
|
||||
7
packages/isomorphic/sha256/package.json
Normal file
7
packages/isomorphic/sha256/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@xrplf/isomorphic/sha256",
|
||||
"private": true,
|
||||
"main": "../dist/sha256",
|
||||
"types": "../dist/sha256",
|
||||
"browser": "../dist/sha256/browser.js"
|
||||
}
|
||||
7
packages/isomorphic/sha512/package.json
Normal file
7
packages/isomorphic/sha512/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@xrplf/isomorphic/sha512",
|
||||
"private": true,
|
||||
"main": "../dist/sha512",
|
||||
"types": "../dist/sha512",
|
||||
"browser": "../dist/sha512/browser.js"
|
||||
}
|
||||
11
packages/isomorphic/src/internal/normalizeInput.ts
Normal file
11
packages/isomorphic/src/internal/normalizeInput.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Input } from './types'
|
||||
|
||||
/**
|
||||
* Normalize a string, number array, or Uint8Array to a string or Uint8Array.
|
||||
* Both node and noble lib functions accept these types.
|
||||
*
|
||||
* @param input - value to normalize
|
||||
*/
|
||||
export default function normalizeInput(input: Input): string | Uint8Array {
|
||||
return Array.isArray(input) ? new Uint8Array(input) : input
|
||||
}
|
||||
33
packages/isomorphic/src/internal/types.ts
Normal file
33
packages/isomorphic/src/internal/types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export type ByteEncodedString = string
|
||||
export type Input = Uint8Array | number[] | ByteEncodedString
|
||||
|
||||
/**
|
||||
* A stripped down isomorphic hash inspired by node's `crypto.Hash`
|
||||
*/
|
||||
export interface Hash {
|
||||
/**
|
||||
* Updates the hash content with the given data,
|
||||
*
|
||||
* @param data - a byte encoded string, an array of numbers or a Uint8Array
|
||||
*/
|
||||
update: (data: Input) => this
|
||||
/**
|
||||
* Calculates the digest of all the data passed to be hashed and returns a Uint8Array
|
||||
*/
|
||||
digest: () => Uint8Array
|
||||
}
|
||||
|
||||
export interface HashFn {
|
||||
/**
|
||||
* Produces a Uint8Array for the given hash contents
|
||||
* Shorthand for calling `create`, `update`, and then `digest`
|
||||
*
|
||||
* @param data - a byte encoded string, an array of numbers or a Uint8Array
|
||||
*/
|
||||
(data: Input): Uint8Array
|
||||
|
||||
/**
|
||||
* Creates a new empty `Hash`.
|
||||
*/
|
||||
create: () => Hash
|
||||
}
|
||||
32
packages/isomorphic/src/internal/wrapCryptoCreateHash.ts
Normal file
32
packages/isomorphic/src/internal/wrapCryptoCreateHash.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createHash } from 'crypto'
|
||||
import { Hash, HashFn, Input } from './types'
|
||||
import normalizeInput from './normalizeInput'
|
||||
|
||||
/**
|
||||
* Wrap createHash from node to provide an interface that is isomorphic
|
||||
*
|
||||
* @param type - the hash name
|
||||
* @param fn - {createHash} the hash factory
|
||||
*/
|
||||
export default function wrapCryptoCreateHash(
|
||||
type: string,
|
||||
fn: typeof createHash,
|
||||
): HashFn {
|
||||
function hashFn(input: Input): Uint8Array {
|
||||
return fn(type).update(normalizeInput(input)).digest()
|
||||
}
|
||||
|
||||
hashFn.create = (): Hash => {
|
||||
const hash = fn(type)
|
||||
return {
|
||||
update(input: Input): Hash {
|
||||
hash.update(normalizeInput(input))
|
||||
return this
|
||||
},
|
||||
digest(): Uint8Array {
|
||||
return hash.digest()
|
||||
},
|
||||
}
|
||||
}
|
||||
return hashFn
|
||||
}
|
||||
28
packages/isomorphic/src/internal/wrapNoble.ts
Normal file
28
packages/isomorphic/src/internal/wrapNoble.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { CHash } from '@noble/hashes/utils'
|
||||
import { Hash, HashFn, Input } from './types'
|
||||
import normalizeInput from './normalizeInput'
|
||||
|
||||
/**
|
||||
* Wrap a CHash object from @noble/hashes to provide a interface that is isomorphic
|
||||
*
|
||||
* @param chash - {CHash} hash function to wrap
|
||||
*/
|
||||
export default function wrapNoble(chash: CHash): HashFn {
|
||||
function wrapped(input: Input): Uint8Array {
|
||||
return chash(normalizeInput(input))
|
||||
}
|
||||
|
||||
wrapped.create = (): Hash => {
|
||||
const hash = chash.create()
|
||||
return {
|
||||
update(input: Input): Hash {
|
||||
hash.update(normalizeInput(input))
|
||||
return this
|
||||
},
|
||||
digest(): Uint8Array {
|
||||
return hash.digest()
|
||||
},
|
||||
}
|
||||
}
|
||||
return wrapped
|
||||
}
|
||||
8
packages/isomorphic/src/ripemd160/browser.ts
Normal file
8
packages/isomorphic/src/ripemd160/browser.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ripemd160 as nobleImpl } from '@noble/hashes/ripemd160'
|
||||
|
||||
import wrapNoble from '../internal/wrapNoble'
|
||||
|
||||
/**
|
||||
* Wrap noble-libs's ripemd160 implementation in HashFn
|
||||
*/
|
||||
export const ripemd160 = wrapNoble(nobleImpl)
|
||||
7
packages/isomorphic/src/ripemd160/index.ts
Normal file
7
packages/isomorphic/src/ripemd160/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createHash } from 'crypto'
|
||||
import wrapCryptoCreateHash from '../internal/wrapCryptoCreateHash'
|
||||
|
||||
/**
|
||||
* Wrap node's native ripemd160 implementation in HashFn
|
||||
*/
|
||||
export const ripemd160 = wrapCryptoCreateHash('ripemd160', createHash)
|
||||
8
packages/isomorphic/src/sha256/browser.ts
Normal file
8
packages/isomorphic/src/sha256/browser.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { sha256 as nobleImpl } from '@noble/hashes/sha256'
|
||||
|
||||
import wrapNoble from '../internal/wrapNoble'
|
||||
|
||||
/**
|
||||
* Wrap noble-libs's sha256 implementation in HashFn
|
||||
*/
|
||||
export const sha256 = wrapNoble(nobleImpl)
|
||||
7
packages/isomorphic/src/sha256/index.ts
Normal file
7
packages/isomorphic/src/sha256/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createHash } from 'crypto'
|
||||
import wrapCryptoCreateHash from '../internal/wrapCryptoCreateHash'
|
||||
|
||||
/**
|
||||
* Wrap node's native sha256 implementation in HashFn
|
||||
*/
|
||||
export const sha256 = wrapCryptoCreateHash('sha256', createHash)
|
||||
8
packages/isomorphic/src/sha512/browser.ts
Normal file
8
packages/isomorphic/src/sha512/browser.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { sha512 as nobleImpl } from '@noble/hashes/sha512'
|
||||
|
||||
import wrapNoble from '../internal/wrapNoble'
|
||||
|
||||
/**
|
||||
* Wrap noble-libs's sha512 implementation in HashFn
|
||||
*/
|
||||
export const sha512 = wrapNoble(nobleImpl)
|
||||
7
packages/isomorphic/src/sha512/index.ts
Normal file
7
packages/isomorphic/src/sha512/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createHash } from 'crypto'
|
||||
import wrapCryptoCreateHash from '../internal/wrapCryptoCreateHash'
|
||||
|
||||
/**
|
||||
* Wrap node's native sha512 implementation in HashFn
|
||||
*/
|
||||
export const sha512 = wrapCryptoCreateHash('sha512', createHash)
|
||||
17
packages/isomorphic/src/utils/browser.ts
Normal file
17
packages/isomorphic/src/utils/browser.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
bytesToHex as nobleBytesToHex,
|
||||
hexToBytes as nobleHexToBytes,
|
||||
randomBytes as nobleRandomBytes,
|
||||
} from '@noble/hashes/utils'
|
||||
import type { BytesToHexFn, HexToBytesFn, RandomBytesFn } from './types'
|
||||
|
||||
/* eslint-disable-next-line func-style -- Typed to ensure uniformity between node and browser implementations and docs */
|
||||
export const bytesToHex: typeof BytesToHexFn = (bytes) => {
|
||||
const hex = nobleBytesToHex(
|
||||
bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes),
|
||||
)
|
||||
return hex.toUpperCase()
|
||||
}
|
||||
|
||||
export const hexToBytes: typeof HexToBytesFn = nobleHexToBytes
|
||||
export const randomBytes: typeof RandomBytesFn = nobleRandomBytes
|
||||
72
packages/isomorphic/src/utils/index.ts
Normal file
72
packages/isomorphic/src/utils/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { randomBytes as cryptoRandomBytes } from 'crypto'
|
||||
import type { BytesToHexFn, HexToBytesFn, RandomBytesFn } from './types'
|
||||
|
||||
const OriginalBuffer = Symbol('OriginalBuffer')
|
||||
|
||||
/**
|
||||
* An extended Uint8Array that incorporates a reference to the original Node.js Buffer.
|
||||
*
|
||||
* When converting a Node.js Buffer to a Uint8Array, there's an optimization that shares
|
||||
* the memory of the original Buffer with the resulting Uint8Array instead of copying data.
|
||||
* The Uint8ArrayWithReference interface is used to attach a reference to the original Buffer, ensuring
|
||||
* its persistence in memory (preventing garbage collection) as long as the Uint8Array exists.
|
||||
* This strategy upholds the ownership semantics of the slice of the ArrayBuffer.
|
||||
*/
|
||||
interface Uint8ArrayWithReference extends Uint8Array {
|
||||
[OriginalBuffer]: Buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Node.js Buffer to a Uint8Array for uniform behavior with browser implementations.
|
||||
*
|
||||
* Choices:
|
||||
* 1. Directly returning the Buffer:
|
||||
* - Operation: Return Buffer as is (a Buffer *IS* an instanceof Uint8Array).
|
||||
* - Pros: Most memory and performance efficient.
|
||||
* - Cons: Violates strict Uint8Array typing and may lead to issues where Buffer-specific features are [ab]used.
|
||||
*
|
||||
* 2. Using `new Uint8Array(buffer)` or `Uint8Array.from(buffer)`:
|
||||
* - Operation: Copies the buffer's data into a new Uint8Array.
|
||||
* - Pros: Ensures data isolation; memory-safe.
|
||||
* - Cons: Less performant due to data duplication.
|
||||
*
|
||||
* 3. Using buf.buffer slice:
|
||||
* - Operation: Shares memory between Buffer and Uint8Array.
|
||||
* - Pros: Performant.
|
||||
* - Cons: Risks with shared memory and potential for invalid references.
|
||||
*
|
||||
* 4. Using buf.buffer slice and keeping a Buffer reference for ownership semantics:
|
||||
* - Operation: Shares memory and associates the original Buffer with the resulting Uint8Array.
|
||||
* - Pros: Performant while ensuring the original Buffer isn't garbage collected.
|
||||
* - Cons: Risks with shared memory but mitigates potential for invalid references.
|
||||
*
|
||||
* The chosen method (4) prioritizes performance by sharing memory while ensuring buffer ownership.
|
||||
*
|
||||
* @param {Buffer} buffer - The Node.js Buffer to convert.
|
||||
* @returns {Uint8Array} Resulting Uint8Array sharing the same memory as the Buffer and maintaining a reference to it.
|
||||
*/
|
||||
function toUint8Array(buffer: Buffer): Uint8Array {
|
||||
const u8Array = new Uint8Array(
|
||||
buffer.buffer.slice(
|
||||
buffer.byteOffset,
|
||||
buffer.byteOffset + buffer.byteLength,
|
||||
),
|
||||
) as Uint8ArrayWithReference
|
||||
u8Array[OriginalBuffer] = buffer
|
||||
return u8Array
|
||||
}
|
||||
|
||||
/* eslint-disable func-style -- Typed to ensure uniformity between node and browser implementations and docs */
|
||||
export const bytesToHex: typeof BytesToHexFn = (bytes) => {
|
||||
const buf = Buffer.from(bytes)
|
||||
return buf.toString('hex').toUpperCase()
|
||||
}
|
||||
|
||||
export const hexToBytes: typeof HexToBytesFn = (hex) => {
|
||||
return toUint8Array(Buffer.from(hex, 'hex'))
|
||||
}
|
||||
|
||||
export const randomBytes: typeof RandomBytesFn = (size) => {
|
||||
return toUint8Array(cryptoRandomBytes(size))
|
||||
}
|
||||
/* eslint-enable func-style */
|
||||
20
packages/isomorphic/src/utils/types.ts
Normal file
20
packages/isomorphic/src/utils/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Convert a UInt8Array to hex. The returned hex will be in all caps.
|
||||
*
|
||||
* @param bytes - {Uint8Array} to convert to hex
|
||||
*/
|
||||
export declare function BytesToHexFn(bytes: Uint8Array | number[]): string
|
||||
|
||||
/**
|
||||
* Convert hex to a Uint8Array.
|
||||
*
|
||||
* @param hex - {string} to convert to a Uint8Array
|
||||
*/
|
||||
export declare function HexToBytesFn(hex: string): Uint8Array
|
||||
|
||||
/**
|
||||
* Create a Uint8Array of the supplied size.
|
||||
*
|
||||
* @param size - number of bytes to generate
|
||||
*/
|
||||
export declare function RandomBytesFn(size: number): Uint8Array
|
||||
16
packages/isomorphic/test/ripemd160.test.ts
Normal file
16
packages/isomorphic/test/ripemd160.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ripemd160 } from '@xrplf/isomorphic/ripemd160'
|
||||
import { bytesToHex } from '@xrplf/isomorphic/utils'
|
||||
|
||||
describe('ripemd160', () => {
|
||||
it('hashes', () => {
|
||||
const hashA = ripemd160('abc')
|
||||
const hashB = ripemd160
|
||||
.create()
|
||||
.update(Uint8Array.from([97, 98, 99]))
|
||||
.digest()
|
||||
const expectedHash = `8EB208F7E05D987A9B044A8E98C6B087F15A0BFC`
|
||||
|
||||
expect(bytesToHex(hashA)).toEqual(expectedHash)
|
||||
expect(bytesToHex(hashB)).toEqual(expectedHash)
|
||||
})
|
||||
})
|
||||
17
packages/isomorphic/test/sha256.test.ts
Normal file
17
packages/isomorphic/test/sha256.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { sha256 } from '@xrplf/isomorphic/sha256'
|
||||
import { bytesToHex } from '@xrplf/isomorphic/utils'
|
||||
|
||||
describe('sha256', () => {
|
||||
it('hashes', () => {
|
||||
const hashA = sha256('abc')
|
||||
const hashB = sha256
|
||||
.create()
|
||||
.update(Uint8Array.from([97, 98, 99]))
|
||||
.digest()
|
||||
const expectedHash =
|
||||
'BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD'
|
||||
|
||||
expect(bytesToHex(hashA)).toEqual(expectedHash)
|
||||
expect(bytesToHex(hashB)).toEqual(expectedHash)
|
||||
})
|
||||
})
|
||||
17
packages/isomorphic/test/sha512.test.ts
Normal file
17
packages/isomorphic/test/sha512.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { sha512 } from '@xrplf/isomorphic/sha512'
|
||||
import { bytesToHex } from '@xrplf/isomorphic/utils'
|
||||
|
||||
describe('sha512', () => {
|
||||
it('hashes', () => {
|
||||
const hashA = sha512('abc')
|
||||
const hashB = sha512
|
||||
.create()
|
||||
.update(Uint8Array.from([97, 98, 99]))
|
||||
.digest()
|
||||
const expectedHash =
|
||||
'DDAF35A193617ABACC417349AE20413112E6FA4E89A97EA20A9EEEE64B55D39A2192992A274FC1A836BA3C23A3FEEBBD454D4423643CE80E2A9AC94FA54CA49F'
|
||||
|
||||
expect(bytesToHex(hashA)).toEqual(expectedHash)
|
||||
expect(bytesToHex(hashB)).toEqual(expectedHash)
|
||||
})
|
||||
})
|
||||
31
packages/isomorphic/test/utils.test.ts
Normal file
31
packages/isomorphic/test/utils.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { bytesToHex, hexToBytes, randomBytes } from '../utils'
|
||||
|
||||
describe('utils', function () {
|
||||
it('randomBytes', () => {
|
||||
expect(randomBytes(16).byteLength).toEqual(16)
|
||||
})
|
||||
|
||||
it('hexToBytes - empty', () => {
|
||||
expect(hexToBytes('')).toEqual(new Uint8Array([]))
|
||||
})
|
||||
|
||||
it('hexToBytes - zero', () => {
|
||||
expect(hexToBytes('000000')).toEqual(new Uint8Array([0, 0, 0]))
|
||||
})
|
||||
|
||||
it('hexToBytes - DEADBEEF', () => {
|
||||
expect(hexToBytes('DEADBEEF')).toEqual(new Uint8Array([222, 173, 190, 239]))
|
||||
})
|
||||
|
||||
it('bytesToHex - DEADBEEF', () => {
|
||||
expect(bytesToHex([222, 173, 190, 239])).toEqual('DEADBEEF')
|
||||
})
|
||||
|
||||
it('bytesToHex - 010203', () => {
|
||||
expect(bytesToHex([1, 2, 3])).toEqual('010203')
|
||||
})
|
||||
|
||||
it('bytesToHex - DEADBEEF (Uint8Array)', () => {
|
||||
expect(bytesToHex(new Uint8Array([222, 173, 190, 239]))).toEqual('DEADBEEF')
|
||||
})
|
||||
})
|
||||
9
packages/isomorphic/test/webpack.config.js
Normal file
9
packages/isomorphic/test/webpack.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
'use strict'
|
||||
const { merge } = require('webpack-merge')
|
||||
const { webpackForTest } = require('../../../weback.test.config')
|
||||
const { getDefaultConfiguration } = require('../../../webpack.config')
|
||||
|
||||
module.exports = merge(
|
||||
getDefaultConfiguration(),
|
||||
webpackForTest('./test/index.ts', __dirname),
|
||||
)
|
||||
7
packages/isomorphic/tsconfig.build.json
Normal file
7
packages/isomorphic/tsconfig.build.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.json"],
|
||||
}
|
||||
4
packages/isomorphic/tsconfig.eslint.json
Normal file
4
packages/isomorphic/tsconfig.eslint.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||
}
|
||||
18
packages/isomorphic/tsconfig.json
Normal file
18
packages/isomorphic/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strictNullChecks": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
7
packages/isomorphic/utils/package.json
Normal file
7
packages/isomorphic/utils/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@xrplf/isomorphic/utils",
|
||||
"private": true,
|
||||
"main": "../dist/utils",
|
||||
"types": "../dist/utils",
|
||||
"browser": "../dist/utils/browser.js"
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
* Bump typescript to 5.x
|
||||
* Remove Node 14 support
|
||||
* Remove `assert` dependency. If you were catching `AssertionError` you need to change to `Error`.
|
||||
* Remove `create-hash` in favor of `@noble/hashes`
|
||||
|
||||
### Changes
|
||||
* Execute test in a browser in addition to node
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"base-x": "^3.0.9",
|
||||
"create-hash": "^1.1.2"
|
||||
"@xrplf/isomorphic": "1.0.0",
|
||||
"base-x": "^3.0.9"
|
||||
},
|
||||
"keywords": [
|
||||
"ripple",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
type Sequence = number[] | Buffer | Uint8Array
|
||||
// Buffer is technically not needed, as a Buffer IS a Uint8Array.
|
||||
// However, for communication purposes it's listed here
|
||||
export type ByteArray = number[] | Uint8Array | Buffer
|
||||
|
||||
/**
|
||||
* Check whether two sequences (e.g. Arrays of numbers) are equal.
|
||||
@@ -6,26 +8,20 @@ type Sequence = number[] | Buffer | Uint8Array
|
||||
* @param arr1 - One of the arrays to compare.
|
||||
* @param arr2 - The other array to compare.
|
||||
*/
|
||||
export function seqEqual(arr1: Sequence, arr2: Sequence): boolean {
|
||||
export function arrayEqual(arr1: ByteArray, arr2: ByteArray): boolean {
|
||||
if (arr1.length !== arr2.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (let i = 0; i < arr1.length; i++) {
|
||||
if (arr1[i] !== arr2[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return arr1.every((value, index) => value === arr2[index])
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a value is a sequence (e.g. Array of numbers).
|
||||
* Check whether a value is a scalar
|
||||
*
|
||||
* @param val - The value to check.
|
||||
*/
|
||||
function isSequence(val: Sequence | number): val is Sequence {
|
||||
return typeof val !== 'number'
|
||||
function isScalar(val: ByteArray | number): val is number {
|
||||
return typeof val === 'number'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,17 +35,9 @@ function isSequence(val: Sequence | number): val is Sequence {
|
||||
* @param args - Concatenate of these args into a single array.
|
||||
* @returns Array of concatenated arguments
|
||||
*/
|
||||
export function concatArgs(...args: Array<number | Sequence>): number[] {
|
||||
const ret: number[] = []
|
||||
|
||||
args.forEach((arg) => {
|
||||
if (isSequence(arg)) {
|
||||
for (const j of arg) {
|
||||
ret.push(j)
|
||||
}
|
||||
} else {
|
||||
ret.push(arg)
|
||||
}
|
||||
export function concatArgs(...args: Array<number | ByteArray>): number[] {
|
||||
return args.flatMap((arg) => {
|
||||
return isScalar(arg) ? [arg] : Array.from(arg)
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
* Codec class
|
||||
*/
|
||||
|
||||
import { sha256 } from '@xrplf/isomorphic/sha256'
|
||||
import baseCodec = require('base-x')
|
||||
import type { BaseConverter } from 'base-x'
|
||||
import createHash = require('create-hash')
|
||||
|
||||
import { seqEqual, concatArgs } from './utils'
|
||||
import { arrayEqual, concatArgs, ByteArray } from './utils'
|
||||
|
||||
class Codec {
|
||||
private readonly _sha256: (bytes: Uint8Array) => Buffer
|
||||
private readonly _sha256: (bytes: ByteArray) => Uint8Array
|
||||
private readonly _alphabet: string
|
||||
private readonly _codec: BaseConverter
|
||||
|
||||
public constructor(options: {
|
||||
sha256: (bytes: Uint8Array) => Buffer
|
||||
sha256: (bytes: ByteArray) => Uint8Array
|
||||
alphabet: string
|
||||
}) {
|
||||
this._sha256 = options.sha256
|
||||
@@ -29,7 +29,7 @@ class Codec {
|
||||
* @param opts - Options object including the version bytes and the expected length of the data to encode.
|
||||
*/
|
||||
public encode(
|
||||
bytes: Buffer,
|
||||
bytes: ByteArray,
|
||||
opts: {
|
||||
versions: number[]
|
||||
expectedLength: number
|
||||
@@ -82,7 +82,7 @@ class Codec {
|
||||
const version: number[] = Array.isArray(versions[i])
|
||||
? (versions[i] as number[])
|
||||
: [versions[i] as number]
|
||||
if (seqEqual(versionBytes, version)) {
|
||||
if (arrayEqual(versionBytes, version)) {
|
||||
return {
|
||||
version,
|
||||
bytes: payload,
|
||||
@@ -97,7 +97,7 @@ class Codec {
|
||||
)
|
||||
}
|
||||
|
||||
public encodeChecked(buffer: Buffer): string {
|
||||
public encodeChecked(buffer: ByteArray): string {
|
||||
const check = this._sha256(this._sha256(buffer)).slice(0, 4)
|
||||
return this._encodeRaw(Buffer.from(concatArgs(buffer, check)))
|
||||
}
|
||||
@@ -114,7 +114,7 @@ class Codec {
|
||||
}
|
||||
|
||||
private _encodeVersioned(
|
||||
bytes: Buffer,
|
||||
bytes: ByteArray,
|
||||
versions: number[],
|
||||
expectedLength: number,
|
||||
): string {
|
||||
@@ -124,10 +124,10 @@ class Codec {
|
||||
' Ensure that the bytes are a Buffer.',
|
||||
)
|
||||
}
|
||||
return this.encodeChecked(Buffer.from(concatArgs(versions, bytes)))
|
||||
return this.encodeChecked(concatArgs(versions, bytes))
|
||||
}
|
||||
|
||||
private _encodeRaw(bytes: Buffer): string {
|
||||
private _encodeRaw(bytes: ByteArray): string {
|
||||
return this._codec.encode(bytes)
|
||||
}
|
||||
/* eslint-enable max-lines-per-function */
|
||||
@@ -136,10 +136,10 @@ class Codec {
|
||||
return this._codec.decode(base58string)
|
||||
}
|
||||
|
||||
private _verifyCheckSum(bytes: Buffer): boolean {
|
||||
private _verifyCheckSum(bytes: ByteArray): boolean {
|
||||
const computed = this._sha256(this._sha256(bytes.slice(0, -4))).slice(0, 4)
|
||||
const checksum = bytes.slice(-4)
|
||||
return seqEqual(computed, checksum)
|
||||
return arrayEqual(computed, checksum)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,9 +161,7 @@ const NODE_PUBLIC = 0x1c
|
||||
const ED25519_SEED = [0x01, 0xe1, 0x4b]
|
||||
|
||||
const codecOptions = {
|
||||
sha256(bytes: Uint8Array): Buffer {
|
||||
return createHash('sha256').update(Buffer.from(bytes)).digest()
|
||||
},
|
||||
sha256,
|
||||
alphabet: 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz',
|
||||
}
|
||||
|
||||
@@ -174,7 +172,7 @@ export const codec = codecWithXrpAlphabet
|
||||
// entropy is a Buffer of size 16
|
||||
// type is 'ed25519' or 'secp256k1'
|
||||
export function encodeSeed(
|
||||
entropy: Buffer,
|
||||
entropy: ByteArray,
|
||||
type: 'ed25519' | 'secp256k1',
|
||||
): string {
|
||||
if (entropy.length !== 16) {
|
||||
@@ -210,7 +208,7 @@ export function decodeSeed(
|
||||
return codecWithXrpAlphabet.decode(seed, opts)
|
||||
}
|
||||
|
||||
export function encodeAccountID(bytes: Buffer): string {
|
||||
export function encodeAccountID(bytes: ByteArray): string {
|
||||
const opts = { versions: [ACCOUNT_ID], expectedLength: 20 }
|
||||
return codecWithXrpAlphabet.encode(bytes, opts)
|
||||
}
|
||||
@@ -237,12 +235,12 @@ export function decodeNodePublic(base58string: string): Buffer {
|
||||
return codecWithXrpAlphabet.decode(base58string, opts).bytes
|
||||
}
|
||||
|
||||
export function encodeNodePublic(bytes: Buffer): string {
|
||||
export function encodeNodePublic(bytes: ByteArray): string {
|
||||
const opts = { versions: [NODE_PUBLIC], expectedLength: 33 }
|
||||
return codecWithXrpAlphabet.encode(bytes, opts)
|
||||
}
|
||||
|
||||
export function encodeAccountPublic(bytes: Buffer): string {
|
||||
export function encodeAccountPublic(bytes: ByteArray): string {
|
||||
const opts = { versions: [ACCOUNT_PUBLIC_KEY], expectedLength: 33 }
|
||||
return codecWithXrpAlphabet.encode(bytes, opts)
|
||||
}
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import { seqEqual, concatArgs } from '../src/utils'
|
||||
import { arrayEqual, concatArgs } from '../src/utils'
|
||||
|
||||
it('two sequences are equal', () => {
|
||||
expect(seqEqual([1, 2, 3], [1, 2, 3])).toBe(true)
|
||||
expect(arrayEqual([1, 2, 3], [1, 2, 3])).toBe(true)
|
||||
})
|
||||
|
||||
it('elements must be in the same order', () => {
|
||||
expect(seqEqual([3, 2, 1], [1, 2, 3])).toBe(false)
|
||||
expect(arrayEqual([3, 2, 1], [1, 2, 3])).toBe(false)
|
||||
})
|
||||
|
||||
it('sequences do not need to be the same type', () => {
|
||||
expect(seqEqual(Buffer.from([1, 2, 3]), [1, 2, 3])).toBe(true)
|
||||
expect(seqEqual(Buffer.from([1, 2, 3]), new Uint8Array([1, 2, 3]))).toBe(true)
|
||||
expect(arrayEqual(Buffer.from([1, 2, 3]), [1, 2, 3])).toBe(true)
|
||||
expect(arrayEqual(Buffer.from([1, 2, 3]), new Uint8Array([1, 2, 3]))).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
it('sequences with a single element', () => {
|
||||
expect(seqEqual(Buffer.from([1]), [1])).toBe(true)
|
||||
expect(seqEqual(Buffer.from([1]), new Uint8Array([1]))).toBe(true)
|
||||
expect(arrayEqual(Buffer.from([1]), [1])).toBe(true)
|
||||
expect(arrayEqual(Buffer.from([1]), new Uint8Array([1]))).toBe(true)
|
||||
})
|
||||
|
||||
it('empty sequences', () => {
|
||||
expect(seqEqual(Buffer.from([]), [])).toBe(true)
|
||||
expect(seqEqual(Buffer.from([]), new Uint8Array([]))).toBe(true)
|
||||
expect(arrayEqual(Buffer.from([]), [])).toBe(true)
|
||||
expect(arrayEqual(Buffer.from([]), new Uint8Array([]))).toBe(true)
|
||||
})
|
||||
|
||||
it('plain numbers are concatenated', () => {
|
||||
|
||||
@@ -14,5 +14,13 @@
|
||||
"declaration": true,
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"references": [
|
||||
{
|
||||
"path": "../isomorphic/tsconfig.build.json"
|
||||
}
|
||||
],
|
||||
"files": [],
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* Remove Node 14 support
|
||||
* Remove decimal.js and big-integer. Use `BigNumber` from `bignumber.js` instead of `Decimal` and the native `BigInt` instead of `bigInt`.
|
||||
* Remove `assert` dependency. If you were catching `AssertionError` you need to change to `Error`.
|
||||
|
||||
* Remove `create-hash` in favor of `@noble/hashes`
|
||||
|
||||
## 1.11.0 (2023-11-30)
|
||||
### Added
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"buffer": "6.0.3",
|
||||
"@xrplf/isomorphic": "1.0.0",
|
||||
"bignumber.js": "^9.0.0",
|
||||
"create-hash": "^1.2.0",
|
||||
"ripple-address-codec": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { HashPrefix } from './hash-prefixes'
|
||||
import createHash = require('create-hash')
|
||||
import { Hash256 } from './types/hash-256'
|
||||
import { Hash256 } from './types'
|
||||
import { BytesList } from './serdes/binary-serializer'
|
||||
import { Buffer } from 'buffer/'
|
||||
import { sha512 } from '@xrplf/isomorphic/sha512'
|
||||
|
||||
/**
|
||||
* Class for hashing with SHA512
|
||||
* @extends BytesList So SerializedTypes can write bytes to a Sha512Half
|
||||
*/
|
||||
class Sha512Half extends BytesList {
|
||||
private hash = createHash('sha512')
|
||||
private hash = sha512.create()
|
||||
|
||||
/**
|
||||
* Construct a new Sha512Hash and write bytes this.hash
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../ripple-address-codec/tsconfig.json"
|
||||
"path": "../isomorphic/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../ripple-address-codec/tsconfig.build.json"
|
||||
}
|
||||
],
|
||||
"include": [
|
||||
|
||||
@@ -26,6 +26,9 @@ module.exports = {
|
||||
extends: ['@xrplf/eslint-config/base'],
|
||||
|
||||
rules: {
|
||||
// TODO: put in @xrplf/eslint-config/base ?
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
|
||||
// ** TODO **
|
||||
// all of the below are turned off for now during the migration to a
|
||||
// monorepo. They need to actually be addressed!
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
* Remove Node 14 support
|
||||
* Remove `assert` dependency. If you were catching `AssertionError` you need to change to `Error`.
|
||||
* Fix `deriveKeypair` ignoring manual decoding algorithm. (Specifying algorithm=`ed25519` in `opts` now works on secrets like `sNa1...`)
|
||||
* Remove `crypto` polyfills, `create-hash`, `elliptic`, `hash.js`, and their many dependencies in favor of `@noble/hashes` and `@nobel/curves`
|
||||
* Remove `bytesToHex` and `hexToBytes`. They can now be found in `@xrplf/isomorphic/utils`
|
||||
* `verifyTransaction` will throw an error if there is no signature
|
||||
* Improved key algorithm detection. It will now throw Errors with helpful messages
|
||||
|
||||
### Changes
|
||||
* Remove `brorand` as a dependency and use `@xrplf/isomorphic` instead.
|
||||
|
||||
## 1.3.1 (2023-09-27)
|
||||
### Fixed
|
||||
|
||||
@@ -18,10 +18,8 @@
|
||||
"test": "test"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": "^5.1.1",
|
||||
"brorand": "^1.0.5",
|
||||
"elliptic": "^6.5.4",
|
||||
"hash.js": "^1.0.3",
|
||||
"@noble/curves": "^1.0.0",
|
||||
"@xrplf/isomorphic": "1.0.0",
|
||||
"ripple-address-codec": "^4.3.1"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/* eslint-disable no-bitwise --
|
||||
* lots of bitwise operators necessary for this */
|
||||
import * as hashjs from 'hash.js'
|
||||
import BigNum = require('bn.js')
|
||||
|
||||
export default class Sha512 {
|
||||
// TODO: type of `hash`?
|
||||
hash: any
|
||||
|
||||
constructor() {
|
||||
this.hash = hashjs.sha512()
|
||||
}
|
||||
|
||||
add(bytes) {
|
||||
this.hash.update(bytes)
|
||||
return this
|
||||
}
|
||||
|
||||
addU32(i) {
|
||||
return this.add([
|
||||
(i >>> 24) & 0xff,
|
||||
(i >>> 16) & 0xff,
|
||||
(i >>> 8) & 0xff,
|
||||
i & 0xff,
|
||||
])
|
||||
}
|
||||
|
||||
finish() {
|
||||
return this.hash.digest()
|
||||
}
|
||||
|
||||
first256() {
|
||||
return this.finish().slice(0, 32)
|
||||
}
|
||||
|
||||
first256BN() {
|
||||
return new BigNum(this.first256())
|
||||
}
|
||||
}
|
||||
@@ -1,168 +1,106 @@
|
||||
import brorand = require('brorand')
|
||||
import * as hashjs from 'hash.js'
|
||||
import * as elliptic from 'elliptic'
|
||||
import {
|
||||
decodeNodePublic,
|
||||
decodeSeed,
|
||||
encodeAccountID,
|
||||
encodeSeed,
|
||||
} from 'ripple-address-codec'
|
||||
import { ripemd160 } from '@xrplf/isomorphic/ripemd160'
|
||||
import { sha256 } from '@xrplf/isomorphic/sha256'
|
||||
import { hexToBytes, randomBytes } from '@xrplf/isomorphic/utils'
|
||||
|
||||
import * as addressCodec from 'ripple-address-codec'
|
||||
import { derivePrivateKey, accountPublicFromPublicGenerator } from './secp256k1'
|
||||
import * as utils from './utils'
|
||||
import { accountPublicFromPublicGenerator } from './signing-schemes/secp256k1/utils'
|
||||
import Sha512 from './utils/Sha512'
|
||||
import assert from './utils/assert'
|
||||
import type { Algorithm, HexString, KeyPair, SigningScheme } from './types'
|
||||
import {
|
||||
getAlgorithmFromPrivateKey,
|
||||
getAlgorithmFromPublicKey,
|
||||
} from './utils/getAlgorithmFromKey'
|
||||
|
||||
const Ed25519 = elliptic.eddsa('ed25519')
|
||||
const Secp256k1 = elliptic.ec('secp256k1')
|
||||
import secp256k1 from './signing-schemes/secp256k1'
|
||||
import ed25519 from './signing-schemes/ed25519'
|
||||
|
||||
const { hexToBytes } = utils
|
||||
const { bytesToHex } = utils
|
||||
function getSigningScheme(algorithm: Algorithm): SigningScheme {
|
||||
const schemes = { 'ecdsa-secp256k1': secp256k1, ed25519 }
|
||||
return schemes[algorithm]
|
||||
}
|
||||
|
||||
function generateSeed(
|
||||
options: {
|
||||
entropy?: Uint8Array
|
||||
algorithm?: 'ed25519' | 'ecdsa-secp256k1'
|
||||
algorithm?: Algorithm
|
||||
} = {},
|
||||
): string {
|
||||
if (!(!options.entropy || options.entropy.length >= 16)) {
|
||||
throw new Error('entropy too short')
|
||||
}
|
||||
const entropy = options.entropy ? options.entropy.slice(0, 16) : brorand(16)
|
||||
assert.ok(
|
||||
!options.entropy || options.entropy.length >= 16,
|
||||
'entropy too short',
|
||||
)
|
||||
const entropy = options.entropy
|
||||
? options.entropy.slice(0, 16)
|
||||
: randomBytes(16)
|
||||
const type = options.algorithm === 'ed25519' ? 'ed25519' : 'secp256k1'
|
||||
return addressCodec.encodeSeed(Buffer.from(entropy), type)
|
||||
}
|
||||
|
||||
function hash(message): number[] {
|
||||
return hashjs.sha512().update(message).digest().slice(0, 32)
|
||||
}
|
||||
|
||||
const secp256k1 = {
|
||||
deriveKeypair(
|
||||
entropy: Uint8Array,
|
||||
options?: object,
|
||||
): {
|
||||
privateKey: string
|
||||
publicKey: string
|
||||
} {
|
||||
const prefix = '00'
|
||||
|
||||
const privateKey =
|
||||
prefix + derivePrivateKey(entropy, options).toString(16, 64).toUpperCase()
|
||||
|
||||
const publicKey = bytesToHex(
|
||||
Secp256k1.keyFromPrivate(privateKey.slice(2))
|
||||
.getPublic()
|
||||
.encodeCompressed(),
|
||||
)
|
||||
return { privateKey, publicKey }
|
||||
},
|
||||
|
||||
sign(message, privateKey): string {
|
||||
return bytesToHex(
|
||||
Secp256k1.sign(hash(message), hexToBytes(privateKey), {
|
||||
canonical: true,
|
||||
}).toDER(),
|
||||
)
|
||||
},
|
||||
|
||||
verify(message, signature, publicKey): boolean {
|
||||
return Secp256k1.verify(hash(message), signature, hexToBytes(publicKey))
|
||||
},
|
||||
}
|
||||
|
||||
const ed25519 = {
|
||||
deriveKeypair(entropy: Uint8Array): {
|
||||
privateKey: string
|
||||
publicKey: string
|
||||
} {
|
||||
const prefix = 'ED'
|
||||
const rawPrivateKey = hash(entropy)
|
||||
const privateKey = prefix + bytesToHex(rawPrivateKey)
|
||||
const publicKey =
|
||||
prefix + bytesToHex(Ed25519.keyFromSecret(rawPrivateKey).pubBytes())
|
||||
return { privateKey, publicKey }
|
||||
},
|
||||
|
||||
sign(message, privateKey): string {
|
||||
// caution: Ed25519.sign interprets all strings as hex, stripping
|
||||
// any non-hex characters without warning
|
||||
if (!Array.isArray(message)) {
|
||||
throw new Error('message must be array of octets')
|
||||
}
|
||||
return bytesToHex(
|
||||
Ed25519.sign(message, hexToBytes(privateKey).slice(1)).toBytes(),
|
||||
)
|
||||
},
|
||||
|
||||
verify(message, signature, publicKey): boolean {
|
||||
return Ed25519.verify(
|
||||
message,
|
||||
hexToBytes(signature),
|
||||
hexToBytes(publicKey).slice(1),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
function select(algorithm): any {
|
||||
const methods = { 'ecdsa-secp256k1': secp256k1, ed25519 }
|
||||
return methods[algorithm]
|
||||
return encodeSeed(entropy, type)
|
||||
}
|
||||
|
||||
function deriveKeypair(
|
||||
seed: string,
|
||||
options?: {
|
||||
algorithm?: 'ed25519' | 'ecdsa-secp256k1'
|
||||
algorithm?: Algorithm
|
||||
validator?: boolean
|
||||
accountIndex?: number
|
||||
},
|
||||
): {
|
||||
publicKey: string
|
||||
privateKey: string
|
||||
} {
|
||||
const decoded = addressCodec.decodeSeed(seed)
|
||||
): KeyPair {
|
||||
const decoded = decodeSeed(seed)
|
||||
const proposedAlgorithm = options?.algorithm ?? decoded.type
|
||||
const algorithm =
|
||||
proposedAlgorithm === 'ed25519' ? 'ed25519' : 'ecdsa-secp256k1'
|
||||
const method = select(algorithm)
|
||||
const keypair = method.deriveKeypair(decoded.bytes, options)
|
||||
const messageToVerify = hash('This test message should verify.')
|
||||
const signature = method.sign(messageToVerify, keypair.privateKey)
|
||||
const scheme = getSigningScheme(algorithm)
|
||||
const keypair = scheme.deriveKeypair(decoded.bytes, options)
|
||||
const messageToVerify = Sha512.half('This test message should verify.')
|
||||
const signature = scheme.sign(messageToVerify, keypair.privateKey)
|
||||
/* istanbul ignore if */
|
||||
if (method.verify(messageToVerify, signature, keypair.publicKey) !== true) {
|
||||
if (!scheme.verify(messageToVerify, signature, keypair.publicKey)) {
|
||||
throw new Error('derived keypair did not generate verifiable signature')
|
||||
}
|
||||
return keypair
|
||||
}
|
||||
|
||||
function getAlgorithmFromKey(key): 'ed25519' | 'ecdsa-secp256k1' {
|
||||
const bytes = hexToBytes(key)
|
||||
return bytes.length === 33 && bytes[0] === 0xed
|
||||
? 'ed25519'
|
||||
: 'ecdsa-secp256k1'
|
||||
function sign(messageHex: HexString, privateKey: HexString): HexString {
|
||||
const algorithm = getAlgorithmFromPrivateKey(privateKey)
|
||||
return getSigningScheme(algorithm).sign(hexToBytes(messageHex), privateKey)
|
||||
}
|
||||
|
||||
function sign(messageHex, privateKey): string {
|
||||
const algorithm = getAlgorithmFromKey(privateKey)
|
||||
return select(algorithm).sign(hexToBytes(messageHex), privateKey)
|
||||
}
|
||||
|
||||
function verify(messageHex, signature, publicKey): boolean {
|
||||
const algorithm = getAlgorithmFromKey(publicKey)
|
||||
return select(algorithm).verify(hexToBytes(messageHex), signature, publicKey)
|
||||
}
|
||||
|
||||
function deriveAddressFromBytes(publicKeyBytes: Buffer): string {
|
||||
return addressCodec.encodeAccountID(
|
||||
utils.computePublicKeyHash(publicKeyBytes),
|
||||
function verify(
|
||||
messageHex: HexString,
|
||||
signature: HexString,
|
||||
publicKey: HexString,
|
||||
): boolean {
|
||||
const algorithm = getAlgorithmFromPublicKey(publicKey)
|
||||
return getSigningScheme(algorithm).verify(
|
||||
hexToBytes(messageHex),
|
||||
signature,
|
||||
publicKey,
|
||||
)
|
||||
}
|
||||
|
||||
function deriveAddress(publicKey): string {
|
||||
return deriveAddressFromBytes(Buffer.from(hexToBytes(publicKey)))
|
||||
function computePublicKeyHash(publicKeyBytes: Uint8Array): Uint8Array {
|
||||
return ripemd160(sha256(publicKeyBytes))
|
||||
}
|
||||
|
||||
function deriveNodeAddress(publicKey): string {
|
||||
const generatorBytes = addressCodec.decodeNodePublic(publicKey)
|
||||
function deriveAddressFromBytes(publicKeyBytes: Uint8Array): string {
|
||||
return encodeAccountID(computePublicKeyHash(publicKeyBytes))
|
||||
}
|
||||
|
||||
function deriveAddress(publicKey: string): string {
|
||||
return deriveAddressFromBytes(hexToBytes(publicKey))
|
||||
}
|
||||
|
||||
function deriveNodeAddress(publicKey: string): string {
|
||||
const generatorBytes = decodeNodePublic(publicKey)
|
||||
const accountPublicBytes = accountPublicFromPublicGenerator(generatorBytes)
|
||||
return deriveAddressFromBytes(accountPublicBytes)
|
||||
}
|
||||
|
||||
const { decodeSeed } = addressCodec
|
||||
|
||||
export {
|
||||
generateSeed,
|
||||
deriveKeypair,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ed25519 as nobleEd25519 } from '@noble/curves/ed25519'
|
||||
import { bytesToHex } from '@xrplf/isomorphic/utils'
|
||||
|
||||
import type { HexString, SigningScheme } from '../../types'
|
||||
import assert from '../../utils/assert'
|
||||
import Sha512 from '../../utils/Sha512'
|
||||
|
||||
const ED_PREFIX = 'ED'
|
||||
|
||||
const ed25519: SigningScheme = {
|
||||
deriveKeypair(entropy: Uint8Array): {
|
||||
privateKey: string
|
||||
publicKey: string
|
||||
} {
|
||||
const rawPrivateKey = Sha512.half(entropy)
|
||||
const privateKey = ED_PREFIX + bytesToHex(rawPrivateKey)
|
||||
const publicKey =
|
||||
ED_PREFIX + bytesToHex(nobleEd25519.getPublicKey(rawPrivateKey))
|
||||
return { privateKey, publicKey }
|
||||
},
|
||||
|
||||
sign(message: Uint8Array, privateKey: HexString): string {
|
||||
assert.ok(message instanceof Uint8Array, 'message must be array of octets')
|
||||
assert.ok(
|
||||
privateKey.length === 66,
|
||||
'private key must be 33 bytes including prefix',
|
||||
)
|
||||
return bytesToHex(nobleEd25519.sign(message, privateKey.slice(2)))
|
||||
},
|
||||
|
||||
verify(
|
||||
message: Uint8Array,
|
||||
signature: HexString,
|
||||
publicKey: string,
|
||||
): boolean {
|
||||
// Unlikely to be triggered as these are internal and guarded by getAlgorithmFromKey
|
||||
assert.ok(
|
||||
publicKey.length === 66,
|
||||
'public key must be 33 bytes including prefix',
|
||||
)
|
||||
return nobleEd25519.verify(
|
||||
signature,
|
||||
message,
|
||||
// Remove the 0xED prefix
|
||||
publicKey.slice(2),
|
||||
// By default, set zip215 to false for compatibility reasons.
|
||||
// ZIP 215 is a stricter Ed25519 signature verification scheme.
|
||||
// However, setting it to false adheres to the more commonly used
|
||||
// RFC8032 / NIST186-5 standards, making it compatible with systems
|
||||
// like the XRP Ledger.
|
||||
{ zip215: false },
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default ed25519
|
||||
@@ -0,0 +1,64 @@
|
||||
import { numberToBytesBE } from '@noble/curves/abstract/utils'
|
||||
import { secp256k1 as nobleSecp256k1 } from '@noble/curves/secp256k1'
|
||||
import { bytesToHex } from '@xrplf/isomorphic/utils'
|
||||
|
||||
import type {
|
||||
DeriveKeyPairOptions,
|
||||
HexString,
|
||||
SigningScheme,
|
||||
} from '../../types'
|
||||
|
||||
import { derivePrivateKey } from './utils'
|
||||
import assert from '../../utils/assert'
|
||||
import Sha512 from '../../utils/Sha512'
|
||||
|
||||
const SECP256K1_PREFIX = '00'
|
||||
|
||||
const secp256k1: SigningScheme = {
|
||||
deriveKeypair(
|
||||
entropy: Uint8Array,
|
||||
options?: DeriveKeyPairOptions,
|
||||
): {
|
||||
privateKey: string
|
||||
publicKey: string
|
||||
} {
|
||||
const derived = derivePrivateKey(entropy, options)
|
||||
const privateKey =
|
||||
SECP256K1_PREFIX + bytesToHex(numberToBytesBE(derived, 32))
|
||||
|
||||
const publicKey = bytesToHex(nobleSecp256k1.getPublicKey(derived, true))
|
||||
return { privateKey, publicKey }
|
||||
},
|
||||
|
||||
sign(message: Uint8Array, privateKey: HexString): string {
|
||||
// Some callers pass the privateKey with the prefix, others without.
|
||||
// @noble/curves will throw if the key is not exactly 32 bytes, so we
|
||||
// normalize it before passing to the sign method.
|
||||
assert.ok(
|
||||
(privateKey.length === 66 && privateKey.startsWith(SECP256K1_PREFIX)) ||
|
||||
privateKey.length === 64,
|
||||
)
|
||||
const normedPrivateKey =
|
||||
privateKey.length === 66 ? privateKey.slice(2) : privateKey
|
||||
return nobleSecp256k1
|
||||
.sign(Sha512.half(message), normedPrivateKey, {
|
||||
// "Canonical" signatures
|
||||
lowS: true,
|
||||
// Would fail tests if signatures aren't deterministic
|
||||
extraEntropy: undefined,
|
||||
})
|
||||
.toDERHex(true)
|
||||
.toUpperCase()
|
||||
},
|
||||
|
||||
verify(
|
||||
message: Uint8Array,
|
||||
signature: HexString,
|
||||
publicKey: HexString,
|
||||
): boolean {
|
||||
const decoded = nobleSecp256k1.Signature.fromDER(signature)
|
||||
return nobleSecp256k1.verify(decoded, Sha512.half(message), publicKey)
|
||||
},
|
||||
}
|
||||
|
||||
export default secp256k1
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as elliptic from 'elliptic'
|
||||
import { secp256k1 } from '@noble/curves/secp256k1'
|
||||
|
||||
import Sha512 from './Sha512'
|
||||
import Sha512 from '../../utils/Sha512'
|
||||
|
||||
const secp256k1 = elliptic.ec('secp256k1')
|
||||
const ZERO = BigInt(0)
|
||||
|
||||
function deriveScalar(bytes, discrim?: number) {
|
||||
const order = secp256k1.curve.n
|
||||
for (let i = 0; i <= 0xffffffff; i++) {
|
||||
// We hash the bytes to find a 256 bit number, looping until we are sure it
|
||||
function deriveScalar(bytes: Uint8Array, discrim?: number): bigint {
|
||||
const order = secp256k1.CURVE.n
|
||||
for (let i = 0; i <= 0xffff_ffff; i++) {
|
||||
// We hash the bytes to find a 256-bit number, looping until we are sure it
|
||||
// is less than the order of the curve.
|
||||
const hasher = new Sha512().add(bytes)
|
||||
// If the optional discriminator index was passed in, update the hash.
|
||||
@@ -15,9 +15,9 @@ function deriveScalar(bytes, discrim?: number) {
|
||||
hasher.addU32(discrim)
|
||||
}
|
||||
hasher.addU32(i)
|
||||
const key = hasher.first256BN()
|
||||
const key = hasher.first256BigInt()
|
||||
/* istanbul ignore else */
|
||||
if (key.cmpn(0) > 0 && key.cmp(order) < 0) {
|
||||
if (key > ZERO && key < order) {
|
||||
return key
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ function deriveScalar(bytes, discrim?: number) {
|
||||
// How often will an (essentially) random number generated by Sha512 be larger than that?
|
||||
// There's 2^32 chances (the for loop) to get a number smaller than the order,
|
||||
// and it's rare that you'll even get past the first loop iteration.
|
||||
// Note that in TypeScript we actually need the throw, otherwise the function signature would be BN | undefined
|
||||
// Note that in TypeScript we actually need the throw, otherwise the function signature would be bigint | undefined
|
||||
//
|
||||
/* istanbul ignore next */
|
||||
throw new Error('impossible unicorn ;)')
|
||||
@@ -39,18 +39,18 @@ function deriveScalar(bytes, discrim?: number) {
|
||||
* @param [opts.accountIndex=0] - The account number to generate.
|
||||
* @param [opts.validator=false] - Generate root key-pair,
|
||||
* as used by validators.
|
||||
* @returns {bn.js} 256 bit scalar value.
|
||||
* @returns {bigint} 256 bit scalar value.
|
||||
*
|
||||
*/
|
||||
export function derivePrivateKey(
|
||||
seed,
|
||||
seed: Uint8Array,
|
||||
opts: {
|
||||
validator?: boolean
|
||||
accountIndex?: number
|
||||
} = {},
|
||||
) {
|
||||
): bigint {
|
||||
const root = opts.validator
|
||||
const order = secp256k1.curve.n
|
||||
const order = secp256k1.CURVE.n
|
||||
|
||||
// This private generator represents the `root` private key, and is what's
|
||||
// used by validators for signing when a keypair is generated from a seed.
|
||||
@@ -59,19 +59,18 @@ export function derivePrivateKey(
|
||||
// As returned by validation_create for a given seed
|
||||
return privateGen
|
||||
}
|
||||
const publicGen = secp256k1.g.mul(privateGen)
|
||||
const publicGen =
|
||||
secp256k1.ProjectivePoint.BASE.multiply(privateGen).toRawBytes(true)
|
||||
// A seed can generate many keypairs as a function of the seed and a uint32.
|
||||
// Almost everyone just uses the first account, `0`.
|
||||
const accountIndex = opts.accountIndex || 0
|
||||
return deriveScalar(publicGen.encodeCompressed(), accountIndex)
|
||||
.add(privateGen)
|
||||
.mod(order)
|
||||
return (deriveScalar(publicGen, accountIndex) + privateGen) % order
|
||||
}
|
||||
|
||||
export function accountPublicFromPublicGenerator(publicGenBytes) {
|
||||
const rootPubPoint = secp256k1.curve.decodePoint(publicGenBytes)
|
||||
export function accountPublicFromPublicGenerator(publicGenBytes: Uint8Array) {
|
||||
const rootPubPoint = secp256k1.ProjectivePoint.fromHex(publicGenBytes)
|
||||
const scalar = deriveScalar(publicGenBytes, 0)
|
||||
const point = secp256k1.g.mul(scalar)
|
||||
const point = secp256k1.ProjectivePoint.BASE.multiply(scalar)
|
||||
const offset = rootPubPoint.add(point)
|
||||
return offset.encodeCompressed()
|
||||
return offset.toRawBytes(true)
|
||||
}
|
||||
35
packages/ripple-keypairs/src/types.ts
Normal file
35
packages/ripple-keypairs/src/types.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export type HexString = string
|
||||
export type Algorithm = 'ecdsa-secp256k1' | 'ed25519'
|
||||
export type KeyType = 'private' | 'public'
|
||||
|
||||
export interface KeyPair {
|
||||
privateKey: HexString
|
||||
publicKey: HexString
|
||||
}
|
||||
|
||||
export interface DeriveKeyPairOptions {
|
||||
validator?: boolean
|
||||
accountIndex?: number
|
||||
}
|
||||
|
||||
export interface SigningScheme {
|
||||
deriveKeypair: (
|
||||
entropy: Uint8Array,
|
||||
options?: DeriveKeyPairOptions,
|
||||
) => KeyPair
|
||||
|
||||
sign: (
|
||||
// deriveKeyPair creates a Sha512.half as Uint8Array so that's why it takes this
|
||||
// though it /COULD/ take HexString as well
|
||||
// for consistency it should be Uint8Array | HexString everywhere,
|
||||
// or HexString everywhere
|
||||
message: Uint8Array,
|
||||
privateKey: HexString,
|
||||
) => HexString
|
||||
|
||||
verify: (
|
||||
message: Uint8Array,
|
||||
signature: HexString,
|
||||
publicKey: HexString,
|
||||
) => boolean
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import * as hashjs from 'hash.js'
|
||||
import BN = require('bn.js')
|
||||
|
||||
function bytesToHex(a: Iterable<number> | ArrayLike<number>): string {
|
||||
return Array.from(a, (byteValue) => {
|
||||
const hex = byteValue.toString(16).toUpperCase()
|
||||
return hex.length > 1 ? hex : `0${hex}`
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function hexToBytes(a): number[] {
|
||||
if (a.length % 2 !== 0) {
|
||||
throw new Error()
|
||||
}
|
||||
// Special-case length zero to return [].
|
||||
// BN.toArray intentionally returns [0] rather than [] for length zero,
|
||||
// which may make sense for BigNum data, but not for byte strings.
|
||||
return a.length === 0 ? [] : new BN(a, 16).toArray(null, a.length / 2)
|
||||
}
|
||||
|
||||
function computePublicKeyHash(publicKeyBytes: Buffer): Buffer {
|
||||
const hash256 = hashjs.sha256().update(publicKeyBytes).digest()
|
||||
|
||||
const hash160 = hashjs.ripemd160().update(hash256).digest()
|
||||
return Buffer.from(hash160)
|
||||
}
|
||||
|
||||
export { bytesToHex, hexToBytes, computePublicKeyHash }
|
||||
36
packages/ripple-keypairs/src/utils/Sha512.ts
Normal file
36
packages/ripple-keypairs/src/utils/Sha512.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { sha512 } from '@xrplf/isomorphic/sha512'
|
||||
import { bytesToNumberBE } from '@noble/curves/abstract/utils'
|
||||
|
||||
type Input = Uint8Array | number[] | string
|
||||
|
||||
export default class Sha512 {
|
||||
// instantiate empty sha512 hash
|
||||
hash = sha512.create()
|
||||
|
||||
static half(input: Input): Uint8Array {
|
||||
return new Sha512().add(input).first256()
|
||||
}
|
||||
|
||||
add(bytes: Input): this {
|
||||
this.hash.update(bytes)
|
||||
return this
|
||||
}
|
||||
|
||||
addU32(i: number): this {
|
||||
const buffer = new Uint8Array(4)
|
||||
new DataView(buffer.buffer).setUint32(0, i)
|
||||
return this.add(buffer)
|
||||
}
|
||||
|
||||
finish(): Uint8Array {
|
||||
return this.hash.digest()
|
||||
}
|
||||
|
||||
first256(): Uint8Array {
|
||||
return this.finish().slice(0, 32)
|
||||
}
|
||||
|
||||
first256BigInt(): bigint {
|
||||
return bytesToNumberBE(this.first256())
|
||||
}
|
||||
}
|
||||
11
packages/ripple-keypairs/src/utils/assert.ts
Normal file
11
packages/ripple-keypairs/src/utils/assert.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
const assertHelper: {
|
||||
ok: (cond: boolean, message?: string) => asserts cond is true
|
||||
} = {
|
||||
ok(cond, message): asserts cond is true {
|
||||
if (!cond) {
|
||||
throw new Error(message)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default assertHelper
|
||||
121
packages/ripple-keypairs/src/utils/getAlgorithmFromKey.ts
Normal file
121
packages/ripple-keypairs/src/utils/getAlgorithmFromKey.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { Algorithm, HexString, KeyType } from '../types'
|
||||
|
||||
enum Prefix {
|
||||
NONE = -1,
|
||||
ED25519 = 0xed,
|
||||
SECP256K1_PUB_X = 0x02,
|
||||
SECP256K1_PUB_X_ODD_Y = 0x03,
|
||||
SECP256K1_PUB_XY = 0x04,
|
||||
SECP256K1_PRIVATE = 0x00,
|
||||
}
|
||||
|
||||
type CompositeKey = `${KeyType}_${Prefix}_${number}`
|
||||
|
||||
/**
|
||||
* | Curve | Type | Prefix | Length | Description | Algorithm |
|
||||
* |-----------|-------------|:------:|:------:|-------------------------------------------------------|----------------:|
|
||||
* | ed25519 | Private | 0xED | 33 | prefix + Uint256LE (0 < n < order ) | ed25519 |
|
||||
* | ed25519 | Public | 0xED | 33 | prefix + 32 y-bytes | ed25519 |
|
||||
* | secp256k1 | Public (1) | 0x02 | 33 | prefix + 32 x-bytes | ecdsa-secp256k1 |
|
||||
* | secp256k1 | Public (2) | 0x03 | 33 | prefix + 32 x-bytes (y is odd) | ecdsa-secp256k1 |
|
||||
* | secp256k1 | Public (3) | 0x04 | 65 | prefix + 32 x-bytes + 32 y-bytes | ecdsa-secp256k1 |
|
||||
* | secp256k1 | Private (1) | None | 32 | Uint256BE (0 < n < order) | ecdsa-secp256k1 |
|
||||
* | secp256k1 | Private (2) | 0x00 | 33 | prefix + Uint256BE (0 < n < order) | ecdsa-secp256k1 |
|
||||
*
|
||||
* Note: The 0x00 prefix for secpk256k1 Private (2) essentially 0 pads the number
|
||||
* and the interpreted number is the same as 32 bytes.
|
||||
*/
|
||||
const KEY_TYPES: Record<CompositeKey, Algorithm> = {
|
||||
[`private_${Prefix.NONE}_32`]: 'ecdsa-secp256k1',
|
||||
[`private_${Prefix.SECP256K1_PRIVATE}_33`]: 'ecdsa-secp256k1',
|
||||
[`private_${Prefix.ED25519}_33`]: 'ed25519',
|
||||
[`public_${Prefix.ED25519}_33`]: 'ed25519',
|
||||
[`public_${Prefix.SECP256K1_PUB_X}_33`]: 'ecdsa-secp256k1',
|
||||
[`public_${Prefix.SECP256K1_PUB_X_ODD_Y}_33`]: 'ecdsa-secp256k1',
|
||||
[`public_${Prefix.SECP256K1_PUB_XY}_65`]: 'ecdsa-secp256k1',
|
||||
}
|
||||
|
||||
function getKeyInfo(key: HexString) {
|
||||
return {
|
||||
prefix: key.length < 2 ? Prefix.NONE : parseInt(key.slice(0, 2), 16),
|
||||
len: key.length / 2,
|
||||
}
|
||||
}
|
||||
|
||||
function prefixRepr(prefix: Prefix): string {
|
||||
return prefix === Prefix.NONE
|
||||
? 'None'
|
||||
: `0x${prefix.toString(16).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function getValidFormatsTable(type: KeyType) {
|
||||
// No need overkill with renderTable method
|
||||
const padding = 2
|
||||
const colWidth = {
|
||||
algorithm: 'ecdsa-secp256k1'.length + padding,
|
||||
prefix: '0x00'.length + padding,
|
||||
}
|
||||
|
||||
return Object.entries(KEY_TYPES)
|
||||
.filter(([key]) => key.startsWith(type))
|
||||
.map(([key, algorithm]) => {
|
||||
const [, prefix, length] = key.split('_')
|
||||
const paddedAlgo = algorithm.padEnd(colWidth.algorithm)
|
||||
const paddedPrefix = prefixRepr(Number(prefix)).padEnd(colWidth.prefix)
|
||||
return `${paddedAlgo} - Prefix: ${paddedPrefix} Length: ${length} bytes`
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
function keyError({
|
||||
key,
|
||||
type,
|
||||
prefix,
|
||||
len,
|
||||
}: {
|
||||
key: string
|
||||
type: KeyType
|
||||
prefix: number
|
||||
len: number
|
||||
}) {
|
||||
const validFormats = getValidFormatsTable(type)
|
||||
|
||||
return `invalid_key:
|
||||
|
||||
Type: ${type}
|
||||
Key: ${key}
|
||||
Prefix: ${prefixRepr(prefix)}
|
||||
Length: ${len} bytes
|
||||
|
||||
Acceptable ${type} formats are:
|
||||
${validFormats}
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the algorithm associated with a given key (public/private).
|
||||
*
|
||||
* @param key - hexadecimal string representation of the key.
|
||||
* @param type - whether expected key is public or private
|
||||
* @returns Algorithm algorithm for signing/verifying
|
||||
* @throws Error when key is invalid
|
||||
*/
|
||||
export function getAlgorithmFromKey(key: HexString, type: KeyType): Algorithm {
|
||||
const { prefix, len } = getKeyInfo(key)
|
||||
// Special case back compat support for no prefix
|
||||
const usedPrefix = type === 'private' && len === 32 ? Prefix.NONE : prefix
|
||||
const algorithm = KEY_TYPES[`${type}_${usedPrefix}_${len}`]
|
||||
|
||||
if (!algorithm) {
|
||||
throw new Error(keyError({ key, type, len, prefix: usedPrefix }))
|
||||
}
|
||||
return algorithm
|
||||
}
|
||||
|
||||
export function getAlgorithmFromPublicKey(key: HexString): Algorithm {
|
||||
return getAlgorithmFromKey(key, 'public')
|
||||
}
|
||||
|
||||
export function getAlgorithmFromPrivateKey(key: HexString): Algorithm {
|
||||
return getAlgorithmFromKey(key, 'private')
|
||||
}
|
||||
81
packages/ripple-keypairs/test/getAlgorithmFromKey.test.ts
Normal file
81
packages/ripple-keypairs/test/getAlgorithmFromKey.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { getAlgorithmFromKey } from '../src/utils/getAlgorithmFromKey'
|
||||
|
||||
function hexData(count: number) {
|
||||
// for our purposes any hex will do
|
||||
return 'a'.repeat(count)
|
||||
}
|
||||
|
||||
describe('getAlgorithmFromKey', () => {
|
||||
it('should return ed25519 for valid ed25519 private key', () => {
|
||||
const privateKey = `ed${hexData(64)}`
|
||||
expect(getAlgorithmFromKey(privateKey, 'private')).toEqual('ed25519')
|
||||
})
|
||||
|
||||
it('should return ed25519 for valid ed25519 public key', () => {
|
||||
const publicKey = `ed${hexData(64)}`
|
||||
expect(getAlgorithmFromKey(publicKey, 'public')).toEqual('ed25519')
|
||||
})
|
||||
|
||||
it('should return ecdsa-secp256k1 for valid secp256k1 private key without prefix', () => {
|
||||
// 32 bytes, no prefix
|
||||
const privateKey = hexData(64)
|
||||
expect(getAlgorithmFromKey(privateKey, 'private')).toEqual(
|
||||
'ecdsa-secp256k1',
|
||||
)
|
||||
})
|
||||
|
||||
it('should return ecdsa-secp256k1 for valid secp256k1 private key with 0x00 prefix', () => {
|
||||
// 33 bytes, 0x00 prefix
|
||||
const privateKey = `00${hexData(64)}`
|
||||
expect(getAlgorithmFromKey(privateKey, 'private')).toEqual(
|
||||
'ecdsa-secp256k1',
|
||||
)
|
||||
})
|
||||
|
||||
it('should return ecdsa-secp256k1 for valid secp256k1 public key with 0x02 prefix', () => {
|
||||
// 33 bytes, 0x02 prefix
|
||||
const publicKey = `02${hexData(64)}`
|
||||
expect(getAlgorithmFromKey(publicKey, 'public')).toEqual('ecdsa-secp256k1')
|
||||
})
|
||||
|
||||
it('should throw error for invalid private key format', () => {
|
||||
// Invalid tag and length
|
||||
const privateKey = `ff${hexData(60)}`
|
||||
expect(() => getAlgorithmFromKey(privateKey, 'private'))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"invalid_key:
|
||||
|
||||
Type: private
|
||||
Key: ffaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
Prefix: 0xff
|
||||
Length: 31 bytes
|
||||
|
||||
Acceptable private formats are:
|
||||
ecdsa-secp256k1 - Prefix: None Length: 32 bytes
|
||||
ecdsa-secp256k1 - Prefix: 0x00 Length: 33 bytes
|
||||
ed25519 - Prefix: 0xed Length: 33 bytes
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should throw error for invalid public key format', () => {
|
||||
// Invalid tag and length
|
||||
const publicKey = `ff${hexData(60)}`
|
||||
expect(() => getAlgorithmFromKey(publicKey, 'public'))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"invalid_key:
|
||||
|
||||
Type: public
|
||||
Key: ffaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
Prefix: 0xff
|
||||
Length: 31 bytes
|
||||
|
||||
Acceptable public formats are:
|
||||
ed25519 - Prefix: 0xed Length: 33 bytes
|
||||
ecdsa-secp256k1 - Prefix: 0x02 Length: 33 bytes
|
||||
ecdsa-secp256k1 - Prefix: 0x03 Length: 33 bytes
|
||||
ecdsa-secp256k1 - Prefix: 0x04 Length: 65 bytes
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
||||
@@ -1,29 +0,0 @@
|
||||
import assert from 'assert'
|
||||
import * as utils from '../src/utils'
|
||||
|
||||
describe('utils', function () {
|
||||
it('hexToBytes - empty', () => {
|
||||
assert.deepEqual(utils.hexToBytes(''), [])
|
||||
})
|
||||
|
||||
it('hexToBytes - zero', () => {
|
||||
assert.deepEqual(utils.hexToBytes('000000'), [0, 0, 0])
|
||||
})
|
||||
|
||||
it('hexToBytes - DEADBEEF', () => {
|
||||
assert.deepEqual(utils.hexToBytes('DEADBEEF'), [222, 173, 190, 239])
|
||||
})
|
||||
|
||||
it('bytesToHex - DEADBEEF', () => {
|
||||
assert.deepEqual(utils.bytesToHex([222, 173, 190, 239]), 'DEADBEEF')
|
||||
})
|
||||
|
||||
it('bytesToHex - DEADBEEF (Uint8Array)', () => {
|
||||
assert.deepEqual(
|
||||
utils.bytesToHex(new Uint8Array([222, 173, 190, 239])),
|
||||
'DEADBEEF',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
export {}
|
||||
@@ -15,8 +15,15 @@
|
||||
"strictNullChecks": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"references": [{
|
||||
"path": "../ripple-address-codec/tsconfig.json"
|
||||
}],
|
||||
"include": ["src/**/*.ts"]
|
||||
"references": [
|
||||
{
|
||||
"path": "../isomorphic/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../ripple-address-codec/tsconfig.build.json"
|
||||
}
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
|
||||
- Add `xrpl-secret-numbers` by @WietseWind to the mono repo.
|
||||
- `unpkg` and `jsdelivr` support was simplified.
|
||||
- Unit tests run in a browser and node.
|
||||
- Remove `brorand` as a dependency and use `@xrplf/isomorphic` instead.
|
||||
|
||||
### BREAKING CHANGES:
|
||||
- `xrpl-secret-numbers` is now `@xrplf/secret-numbers`.
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"test": "test"
|
||||
},
|
||||
"dependencies": {
|
||||
"brorand": "^1.1.0",
|
||||
"@xrplf/isomorphic": "^1.0.0",
|
||||
"ripple-keypairs": "^1.3.0"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import brorand from "brorand";
|
||||
import { randomBytes } from "@xrplf/isomorphic/utils";
|
||||
|
||||
function randomEntropy(): Buffer {
|
||||
return Buffer.from(brorand(16));
|
||||
return Buffer.from(randomBytes(16));
|
||||
}
|
||||
|
||||
function calculateChecksum(position: number, value: number): number {
|
||||
|
||||
@@ -7,18 +7,22 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
|
||||
### Breaking Changes
|
||||
* Bump typescript to 5.x
|
||||
* Remove Node 14 support
|
||||
* Remove `crypto` polyfills, `create-hash`, `elliptic`, `hash.js`, and their many dependencies in favor of `@noble/hashes` and `@nobel/curves`
|
||||
* Remove `bip32` and `bip39` in favor of `@scure/bip32` and `@scure/bip39`
|
||||
* Remove `assert` dependency. If you were catching `AssertionError` you need to change to `Error`
|
||||
* Configuring a proxy:
|
||||
* Instead of passing various parameters on the `ConnectionsOptions` you know specify the `agent` parameter. This object can use be created by libraries such as `https-proxy-agent` or any that implements the `http.Agent`.
|
||||
* This was changed to both support the latest `https-proxy-agent` and to remove the need to include the package in bundlers. Tests will still be done using `https-proxy-agent` and only tested in a node environment which was the only way it was previously supported anyway
|
||||
* Remove `BroadcastClient` which was deprecated
|
||||
* Uses `@xrplf/secret-numbers` instead of `xrpl-secret-numbers`
|
||||
* Improve key algorithm detection. It will now throw Errors with helpful messages
|
||||
* Move `authorizeChannel` from `wallet/signer` to `wallet/authorizeChannel` to solve a circular dependency issue.
|
||||
|
||||
### Bundling Changes
|
||||
* Bundler configurations are much more simplified.
|
||||
* removed the following polyfills:
|
||||
* `assert`
|
||||
* `buffer`
|
||||
* `crypto-browserify`
|
||||
* `https-browserify`
|
||||
* `os-browserify`
|
||||
* `stream-browserify`
|
||||
@@ -26,7 +30,6 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
|
||||
* `url`
|
||||
* `util` - previously added automatically by `webpack`
|
||||
* Removed mappings for:
|
||||
* `ws` to `WsWrapper`
|
||||
* Excluding `https-proxy-agent`
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -22,10 +22,11 @@
|
||||
"ws": "./dist/npm/client/WSWrapper.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scure/bip32": "^1.3.1",
|
||||
"@scure/bip39": "^1.2.1",
|
||||
"@xrplf/isomorphic": "1.0.0",
|
||||
"@xrplf/secret-numbers": "^1.0.0",
|
||||
"bignumber.js": "^9.0.0",
|
||||
"bip32": "^2.0.6",
|
||||
"bip39": "^3.0.4",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"ripple-address-codec": "^4.3.1",
|
||||
"ripple-binary-codec": "^1.11.0",
|
||||
|
||||
26
packages/xrpl/src/Wallet/authorizeChannel.ts
Normal file
26
packages/xrpl/src/Wallet/authorizeChannel.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { encodeForSigningClaim } from 'ripple-binary-codec'
|
||||
import { sign } from 'ripple-keypairs'
|
||||
|
||||
import { Wallet } from './index'
|
||||
|
||||
/**
|
||||
* Creates a signature that can be used to redeem a specific amount of XRP from a payment channel.
|
||||
*
|
||||
* @param wallet - The account that will sign for this payment channel.
|
||||
* @param channelId - An id for the payment channel to redeem XRP from.
|
||||
* @param amount - The amount in drops to redeem.
|
||||
* @returns A signature that can be used to redeem a specific amount of XRP from a payment channel.
|
||||
* @category Utilities
|
||||
*/
|
||||
export function authorizeChannel(
|
||||
wallet: Wallet,
|
||||
channelId: string,
|
||||
amount: string,
|
||||
): string {
|
||||
const signingData = encodeForSigningClaim({
|
||||
channel: channelId,
|
||||
amount,
|
||||
})
|
||||
|
||||
return sign(signingData, wallet.privateKey)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { HDKey } from '@scure/bip32'
|
||||
import { mnemonicToSeedSync, validateMnemonic } from '@scure/bip39'
|
||||
import { wordlist } from '@scure/bip39/wordlists/english'
|
||||
import { bytesToHex } from '@xrplf/isomorphic/utils'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { fromSeed } from 'bip32'
|
||||
import { mnemonicToSeedSync, validateMnemonic } from 'bip39'
|
||||
import {
|
||||
classicAddressToXAddress,
|
||||
isValidXAddress,
|
||||
@@ -8,7 +10,6 @@ import {
|
||||
encodeSeed,
|
||||
} from 'ripple-address-codec'
|
||||
import {
|
||||
decode,
|
||||
encodeForSigning,
|
||||
encodeForMultisigning,
|
||||
encode,
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
deriveAddress,
|
||||
deriveKeypair,
|
||||
generateSeed,
|
||||
verify,
|
||||
sign,
|
||||
} from 'ripple-keypairs'
|
||||
|
||||
@@ -29,12 +29,24 @@ import { omitBy } from '../utils/collections'
|
||||
import { hashSignedTx } from '../utils/hashes/hashLedger'
|
||||
|
||||
import { rfc1751MnemonicToKey } from './rfc1751'
|
||||
import { verifySignature } from './signer'
|
||||
|
||||
const DEFAULT_ALGORITHM: ECDSA = ECDSA.ed25519
|
||||
const DEFAULT_DERIVATION_PATH = "m/44'/144'/0'/0/0"
|
||||
|
||||
function hexFromBuffer(buffer: Buffer): string {
|
||||
return buffer.toString('hex').toUpperCase()
|
||||
type ValidHDKey = HDKey & {
|
||||
privateKey: Uint8Array
|
||||
publicKey: Uint8Array
|
||||
}
|
||||
|
||||
function validateKey(node: HDKey): asserts node is ValidHDKey {
|
||||
if (!(node.privateKey instanceof Uint8Array)) {
|
||||
throw new ValidationError('Unable to derive privateKey from mnemonic input')
|
||||
}
|
||||
|
||||
if (!(node.publicKey instanceof Uint8Array)) {
|
||||
throw new ValidationError('Unable to derive publicKey from mnemonic input')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,25 +244,21 @@ export class Wallet {
|
||||
})
|
||||
}
|
||||
// Otherwise decode using bip39's mnemonic standard
|
||||
if (!validateMnemonic(mnemonic)) {
|
||||
if (!validateMnemonic(mnemonic, wordlist)) {
|
||||
throw new ValidationError(
|
||||
'Unable to parse the given mnemonic using bip39 encoding',
|
||||
)
|
||||
}
|
||||
|
||||
const seed = mnemonicToSeedSync(mnemonic)
|
||||
const masterNode = fromSeed(seed)
|
||||
const node = masterNode.derivePath(
|
||||
const masterNode = HDKey.fromMasterSeed(seed)
|
||||
const node = masterNode.derive(
|
||||
opts.derivationPath ?? DEFAULT_DERIVATION_PATH,
|
||||
)
|
||||
if (node.privateKey === undefined) {
|
||||
throw new ValidationError(
|
||||
'Unable to derive privateKey from mnemonic input',
|
||||
)
|
||||
}
|
||||
validateKey(node)
|
||||
|
||||
const publicKey = hexFromBuffer(node.publicKey)
|
||||
const privateKey = hexFromBuffer(node.privateKey)
|
||||
const publicKey = bytesToHex(node.publicKey)
|
||||
const privateKey = bytesToHex(node.privateKey)
|
||||
return new Wallet(publicKey, `00${privateKey}`, {
|
||||
masterAddress: opts.masterAddress,
|
||||
})
|
||||
@@ -434,15 +442,10 @@ export class Wallet {
|
||||
*
|
||||
* @param signedTransaction - A signed transaction (hex string of signTransaction result) to be verified offline.
|
||||
* @returns Returns true if a signedTransaction is valid.
|
||||
* @throws {Error} Transaction is missing a signature, TxnSignature
|
||||
*/
|
||||
public verifyTransaction(signedTransaction: Transaction | string): boolean {
|
||||
const tx =
|
||||
typeof signedTransaction === 'string'
|
||||
? decode(signedTransaction)
|
||||
: signedTransaction
|
||||
const messageHex: string = encodeForSigning(tx)
|
||||
const signature = tx.TxnSignature
|
||||
return verify(messageHex, signature, this.publicKey)
|
||||
return verifySignature(signedTransaction, this.publicKey)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { BigNumber } from 'bignumber.js'
|
||||
import { decodeAccountID } from 'ripple-address-codec'
|
||||
import {
|
||||
decode,
|
||||
encode,
|
||||
encodeForSigning,
|
||||
encodeForSigningClaim,
|
||||
} from 'ripple-binary-codec'
|
||||
import { sign as signWithKeypair, verify } from 'ripple-keypairs'
|
||||
import { decode, encode, encodeForSigning } from 'ripple-binary-codec'
|
||||
import { verify } from 'ripple-keypairs'
|
||||
|
||||
import { ValidationError } from '../errors'
|
||||
import { Signer } from '../models/common'
|
||||
import { Transaction, validate } from '../models/transactions'
|
||||
|
||||
import { Wallet } from '.'
|
||||
|
||||
/**
|
||||
* Takes several transactions with Signer fields (in object or blob form) and creates a
|
||||
* single transaction with all Signers that then gets signed and returned.
|
||||
@@ -61,42 +54,40 @@ function multisign(transactions: Array<Transaction | string>): string {
|
||||
return encode(getTransactionWithAllSigners(decodedTransactions))
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a signature that can be used to redeem a specific amount of XRP from a payment channel.
|
||||
*
|
||||
* @param wallet - The account that will sign for this payment channel.
|
||||
* @param channelId - An id for the payment channel to redeem XRP from.
|
||||
* @param amount - The amount in drops to redeem.
|
||||
* @returns A signature that can be used to redeem a specific amount of XRP from a payment channel.
|
||||
* @category Utilities
|
||||
*/
|
||||
function authorizeChannel(
|
||||
wallet: Wallet,
|
||||
channelId: string,
|
||||
amount: string,
|
||||
): string {
|
||||
const signingData = encodeForSigningClaim({
|
||||
channel: channelId,
|
||||
amount,
|
||||
})
|
||||
|
||||
return signWithKeypair(signingData, wallet.privateKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the given transaction has a valid signature based on public-key encryption.
|
||||
*
|
||||
* @param tx - A transaction to verify the signature of. (Can be in object or encoded string format).
|
||||
* @param [publicKey] Specific public key to use to verify. If not specified the `SigningPublicKey` of tx will be used.
|
||||
* @returns Returns true if tx has a valid signature, and returns false otherwise.
|
||||
* @throws Error when transaction is missing TxnSignature
|
||||
* @throws Error when publicKey is not provided and transaction is missing SigningPubKey
|
||||
* @category Utilities
|
||||
*/
|
||||
function verifySignature(tx: Transaction | string): boolean {
|
||||
function verifySignature(
|
||||
tx: Transaction | string,
|
||||
publicKey?: string,
|
||||
): boolean {
|
||||
const decodedTx: Transaction = getDecodedTransaction(tx)
|
||||
return verify(
|
||||
encodeForSigning(decodedTx),
|
||||
decodedTx.TxnSignature,
|
||||
decodedTx.SigningPubKey,
|
||||
)
|
||||
let key = publicKey
|
||||
|
||||
// Need a SignedTransaction class where TxnSignature is not optional.
|
||||
if (typeof decodedTx.TxnSignature !== 'string' || !decodedTx.TxnSignature) {
|
||||
throw new Error('Transaction is missing a signature, TxnSignature')
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
// Need a SignedTransaction class where TxnSignature is not optional.
|
||||
if (
|
||||
typeof decodedTx.SigningPubKey !== 'string' ||
|
||||
!decodedTx.SigningPubKey
|
||||
) {
|
||||
throw new Error('Transaction is missing a public key, SigningPubKey')
|
||||
}
|
||||
key = decodedTx.SigningPubKey
|
||||
}
|
||||
|
||||
return verify(encodeForSigning(decodedTx), decodedTx.TxnSignature, key)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,4 +159,4 @@ function getDecodedTransaction(txOrBlob: Transaction | string): Transaction {
|
||||
return decode(txOrBlob) as unknown as Transaction
|
||||
}
|
||||
|
||||
export { authorizeChannel, verifySignature, multisign }
|
||||
export { verifySignature, multisign }
|
||||
|
||||
@@ -23,6 +23,7 @@ export function groupBy<T>(
|
||||
index,
|
||||
arrayReference,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- being safe for js users
|
||||
;(acc[iteratee(value, index, arrayReference)] ||= []).push(value)
|
||||
return acc
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash } from 'crypto'
|
||||
import { sha512 } from '@xrplf/isomorphic/sha512'
|
||||
import { bytesToHex, hexToBytes } from '@xrplf/isomorphic/utils'
|
||||
|
||||
const HASH_SIZE = 64
|
||||
const HASH_BYTES = 32
|
||||
|
||||
/**
|
||||
* Compute a sha512Half Hash of a hex string.
|
||||
@@ -9,11 +10,7 @@ const HASH_SIZE = 64
|
||||
* @returns Hash of hex.
|
||||
*/
|
||||
function sha512Half(hex: string): string {
|
||||
return createHash('sha512')
|
||||
.update(Buffer.from(hex, 'hex'))
|
||||
.digest('hex')
|
||||
.toUpperCase()
|
||||
.slice(0, HASH_SIZE)
|
||||
return bytesToHex(sha512(hexToBytes(hex)).slice(0, HASH_BYTES))
|
||||
}
|
||||
|
||||
export default sha512Half
|
||||
|
||||
29
packages/xrpl/test/wallet/authorizeChannel.test.ts
Normal file
29
packages/xrpl/test/wallet/authorizeChannel.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { assert } from 'chai'
|
||||
|
||||
import { ECDSA, Wallet } from '../../src'
|
||||
import { authorizeChannel } from '../../src/Wallet/authorizeChannel'
|
||||
|
||||
it('authorizeChannel succeeds with secp256k1 seed', function () {
|
||||
const secpWallet = Wallet.fromSeed('snGHNrPbHrdUcszeuDEigMdC1Lyyd', {
|
||||
algorithm: ECDSA.secp256k1,
|
||||
})
|
||||
const channelId =
|
||||
'5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3'
|
||||
const amount = '1000000'
|
||||
|
||||
assert.equal(
|
||||
authorizeChannel(secpWallet, channelId, amount),
|
||||
'304402204E7052F33DDAFAAA55C9F5B132A5E50EE95B2CF68C0902F61DFE77299BC893740220353640B951DCD24371C16868B3F91B78D38B6F3FD1E826413CDF891FA8250AAC',
|
||||
)
|
||||
})
|
||||
|
||||
it('authorizeChannel succeeds with ed25519 seed', function () {
|
||||
const edWallet = Wallet.fromSeed('sEdSuqBPSQaood2DmNYVkwWTn1oQTj2')
|
||||
const channelId =
|
||||
'5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3'
|
||||
const amount = '1000000'
|
||||
assert.equal(
|
||||
authorizeChannel(edWallet, channelId, amount),
|
||||
'7E1C217A3E4B3C107B7A356E665088B4FBA6464C48C58267BEF64975E3375EA338AE22E6714E3F5E734AE33E6B97AAD59058E1E196C1F92346FC1498D0674404',
|
||||
)
|
||||
})
|
||||
@@ -1230,6 +1230,18 @@ describe('Wallet', function () {
|
||||
|
||||
assert.equal(isVerified, true)
|
||||
})
|
||||
|
||||
it('should throw an error when not signed', () => {
|
||||
const wallet = new Wallet(publicKey, privateKey)
|
||||
const decodedTransaction = decode(
|
||||
prepared.signedTransaction,
|
||||
) as unknown as Transaction
|
||||
delete decodedTransaction.TxnSignature
|
||||
|
||||
assert.throws(() => {
|
||||
wallet.verifyTransaction(decodedTransaction)
|
||||
}, `Transaction is missing a signature, TxnSignature`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getXAddress', function () {
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { assert } from 'chai'
|
||||
import { decode, encode } from 'ripple-binary-codec'
|
||||
|
||||
import { ECDSA, Transaction, ValidationError } from '../../src'
|
||||
import { Transaction, ValidationError } from '../../src'
|
||||
import { Wallet } from '../../src/Wallet'
|
||||
import {
|
||||
authorizeChannel,
|
||||
multisign,
|
||||
verifySignature,
|
||||
} from '../../src/Wallet/signer'
|
||||
import { multisign, verifySignature } from '../../src/Wallet/signer'
|
||||
|
||||
const publicKey =
|
||||
'030E58CDD076E798C84755590AAF6237CA8FAE821070A59F648B517A30DC6F589D'
|
||||
@@ -187,38 +183,13 @@ describe('Signer', function () {
|
||||
assert.throws(() => multisign(transactions), /forMultisign/u)
|
||||
})
|
||||
|
||||
it('authorizeChannel succeeds with secp256k1 seed', function () {
|
||||
const secpWallet = Wallet.fromSeed('snGHNrPbHrdUcszeuDEigMdC1Lyyd', {
|
||||
algorithm: ECDSA.secp256k1,
|
||||
})
|
||||
const channelId =
|
||||
'5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3'
|
||||
const amount = '1000000'
|
||||
|
||||
assert.equal(
|
||||
authorizeChannel(secpWallet, channelId, amount),
|
||||
'304402204E7052F33DDAFAAA55C9F5B132A5E50EE95B2CF68C0902F61DFE77299BC893740220353640B951DCD24371C16868B3F91B78D38B6F3FD1E826413CDF891FA8250AAC',
|
||||
)
|
||||
})
|
||||
|
||||
it('authorizeChannel succeeds with ed25519 seed', function () {
|
||||
const edWallet = Wallet.fromSeed('sEdSuqBPSQaood2DmNYVkwWTn1oQTj2')
|
||||
const channelId =
|
||||
'5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3'
|
||||
const amount = '1000000'
|
||||
assert.equal(
|
||||
authorizeChannel(edWallet, channelId, amount),
|
||||
'7E1C217A3E4B3C107B7A356E665088B4FBA6464C48C58267BEF64975E3375EA338AE22E6714E3F5E734AE33E6B97AAD59058E1E196C1F92346FC1498D0674404',
|
||||
)
|
||||
})
|
||||
|
||||
it('verifySignature succeeds for valid signed transaction blob', function () {
|
||||
const signedTx = verifyWallet.sign(tx)
|
||||
|
||||
assert.isTrue(verifySignature(signedTx.tx_blob))
|
||||
})
|
||||
|
||||
it('verify succeeds for valid signed transaction object', function () {
|
||||
it('verifySignature succeeds for valid signed transaction object', function () {
|
||||
const signedTx = verifyWallet.sign(tx)
|
||||
|
||||
assert.isTrue(
|
||||
@@ -226,7 +197,7 @@ describe('Signer', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('verify throws for invalid signing key', function () {
|
||||
it('verifySignature returns false for invalid signing key', function () {
|
||||
const signedTx = verifyWallet.sign(tx)
|
||||
|
||||
const decodedTx = decode(signedTx.tx_blob) as unknown as Transaction
|
||||
@@ -237,4 +208,17 @@ describe('Signer', function () {
|
||||
|
||||
assert.isFalse(verifySignature(decodedTx))
|
||||
})
|
||||
|
||||
it('verifySignature throws for a missing public key', function () {
|
||||
const signedTx = verifyWallet.sign(tx)
|
||||
|
||||
const decodedTx = decode(signedTx.tx_blob) as unknown as Transaction
|
||||
|
||||
// Use a different key for validation
|
||||
delete decodedTx.SigningPubKey
|
||||
|
||||
assert.throws(() => {
|
||||
verifySignature(decodedTx)
|
||||
}, `Transaction is missing a public key, SigningPubKey`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
"include": ["./src/**/*.ts", "./src/**/*.json"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../ripple-address-codec/tsconfig.json"
|
||||
"path": "../isomorphic/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../ripple-address-codec/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../ripple-binary-codec/tsconfig.json"
|
||||
|
||||
@@ -11,13 +11,7 @@ module.exports = merge(getDefaultConfiguration(), {
|
||||
path: path.join(__dirname, 'build/'),
|
||||
filename: `xrpl.default.js`,
|
||||
},
|
||||
plugins: [
|
||||
new webpack.NormalModuleReplacementPlugin(/^ws$/, './WSWrapper'),
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp: /^\.\/wordlists\/(?!english)/,
|
||||
contextRegExp: /bip39\/src$/,
|
||||
}),
|
||||
],
|
||||
plugins: [new webpack.NormalModuleReplacementPlugin(/^ws$/, './WSWrapper')],
|
||||
resolve: {
|
||||
alias: {
|
||||
ws: './dist/npm/client/WSWrapper.js',
|
||||
|
||||
Reference in New Issue
Block a user