Skip to content
This repository has been archived by the owner on Nov 24, 2021. It is now read-only.

Commit

Permalink
Merge pull request #30 from ripple/refactor
Browse files Browse the repository at this point in the history
Refactor and use TypeScript:
  • Loading branch information
intelliot authored Oct 18, 2019
2 parents c89fd27 + 5245327 commit a23cda3
Show file tree
Hide file tree
Showing 17 changed files with 4,740 additions and 66 deletions.
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
language: node_js
node_js:
- 6
- 8
- 10
- 12
script:
- yarn compile
- yarn test
- yarn lint
19 changes: 17 additions & 2 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
# ripple-keypairs Release History

## 0.10.1 (2017-11-10)
## 1.0.0-beta.0 (2019-10-17)

* Refactor and use TypeScript
* Vendor ripple-address-codec (move dependency into this project)
* Add support for Travis CI (.travis.yml)
* Use "dist/*" for distribution files
* Add yarn.lock

## 0.11.0 (2018-10-23)

* Upgrade elliptic (#28)

+ [Verify that generated keypairs can correctly sign a message](https://github.com/ripple/ripple-keypairs/pull/22)
## 0.10.2

* Remove unused devDependencies

## 0.10.1 (2017-11-10)

* [Verify that generated keypairs can correctly sign a message](https://github.com/ripple/ripple-keypairs/pull/22)
3 changes: 0 additions & 3 deletions circle.yml

This file was deleted.

35 changes: 16 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
{
"name": "ripple-keypairs",
"version": "0.10.2",
"description": "ripple key pairs",
"version": "1.0.0-beta.0",
"description": "Cryptographic key pairs for the XRP Ledger",
"files": [
"distrib/npm/*",
"bin/*",
"build/*",
"test/*"
"dist/*"
],
"main": "distrib/npm/",
"main": "dist/",
"directories": {
"test": "test"
},
Expand All @@ -18,9 +15,11 @@
"brorand": "^1.0.5",
"elliptic": "^6.4.0",
"hash.js": "^1.0.3",
"ripple-address-codec": "^2.0.1"
"base-x": "3.0.4",
"create-hash": "1.2.0"
},
"devDependencies": {
"@types/node": "^10.12.0",
"assert-diff": "^1.0.1",
"babel": "^5.8.20",
"babel-core": "^5.8.20",
Expand All @@ -33,23 +32,21 @@
"istanbul": "~0.3.5",
"map-stream": "~0.1.0",
"mocha": "~2.3.3",
"nock": "^2.13.0"
"nock": "^2.13.0",
"tslint": "^5.11.0",
"tslint-eslint-rules": "^5.4.0",
"typescript": "^3.1.3"
},
"scripts": {
"compile": "babel --optional runtime -d distrib/npm/ src/",
"compile": "tsc",
"compile-babel": "babel --optional runtime -d distrib/npm/ src/",
"compile-with-source-maps": "babel --optional runtime -s -t -d distrib/npm/ src/",
"prepublish": "npm test && npm run lint && npm run compile",
"test": "istanbul test _mocha",
"lint": "if ! [ -f eslintrc ]; then curl -o eslintrc 'https://raw.githubusercontent.com/ripple/javascript-style-guide/es6/eslintrc'; echo 'parser: babel-eslint' >> eslintrc; fi; eslint -c eslintrc src/*.js test/*.js"
"test": "tsc && istanbul test _mocha",
"lint": "tslint -p ./"
},
"repository": {
"type": "git",
"url": "git://github.com/ripple/ripple-keypairs.git"
},
"bugs": {
"url": "https://github.com/ripple/ripple-keypairs/issues"
},
"homepage": "https://github.com/ripple/ripple-keypairs#readme",
"license": "ISC",
"readmeFilename": "README.md"
"license": "ISC"
}
31 changes: 18 additions & 13 deletions src/index.js → src/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
'use strict' // eslint-disable-line strict
import * as assert from 'assert'
import * as brorand from 'brorand'
import * as hashjs from 'hash.js'
import * as elliptic from 'elliptic'

import * as addressCodec from './ripple-address-codec'
import {derivePrivateKey, accountPublicFromPublicGenerator} from './secp256k1'
import * as utils from './utils'

const assert = require('assert')
const brorand = require('brorand')
const hashjs = require('hash.js')
const elliptic = require('elliptic')
const Ed25519 = elliptic.eddsa('ed25519')
const Secp256k1 = elliptic.ec('secp256k1')
const addressCodec = require('ripple-address-codec')
const derivePrivateKey = require('./secp256k1').derivePrivateKey
const accountPublicFromPublicGenerator = require('./secp256k1')
.accountPublicFromPublicGenerator
const utils = require('./utils')

const hexToBytes = utils.hexToBytes
const bytesToHex = utils.bytesToHex

function generateSeed(options = {}) {
function generateSeed(options: {
entropy?: Uint8Array,
algorithm?: 'ed25519' | 'secp256k1'
} = {}) {
assert(!options.entropy || options.entropy.length >= 16, 'entropy too short')
const entropy = options.entropy ? options.entropy.slice(0, 16) : brorand(16)
const type = options.algorithm === 'ed25519' ? 'ed25519' : 'secp256k1'
Expand Down Expand Up @@ -99,7 +101,7 @@ function verify(messageHex, signature, publicKey) {
return select(algorithm).verify(hexToBytes(messageHex), signature, publicKey)
}

function deriveAddressFromBytes(publicKeyBytes) {
function deriveAddressFromBytes(publicKeyBytes: Buffer) {
return addressCodec.encodeAccountID(
utils.computePublicKeyHash(publicKeyBytes))
}
Expand All @@ -114,11 +116,14 @@ function deriveNodeAddress(publicKey) {
return deriveAddressFromBytes(accountPublicBytes)
}

const decodeSeed = addressCodec.decodeSeed

module.exports = {
generateSeed,
deriveKeypair,
sign,
verify,
deriveAddress,
deriveNodeAddress
deriveNodeAddress,
decodeSeed
}
220 changes: 220 additions & 0 deletions src/ripple-address-codec/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/**
* Codec class
*/

const baseCodec = require('base-x')
const {seqEqual, concatArgs} = require('./utils')

class Codec {
sha256: (bytes: Uint8Array) => Buffer
alphabet: string
codec: any
base: number

constructor(options: {
sha256: (bytes: Uint8Array) => Buffer,
alphabet: string
}) {
this.sha256 = options.sha256
this.alphabet = options.alphabet
this.codec = baseCodec(this.alphabet)
this.base = this.alphabet.length
}

/**
* Encoder.
*
* @param bytes Buffer of data to encode.
* @param opts Options object including the version bytes and the expected length of the data to encode.
*/
encode(bytes: number[], opts: {
versions: number[],
expectedLength: number
}) {
const versions = opts.versions
return this.encodeVersioned(bytes, versions, opts.expectedLength)
}

encodeVersioned(bytes: number[], versions: number[], expectedLength: number) {
if (expectedLength && bytes.length !== expectedLength) {
throw new Error('unexpected_payload_length: bytes.length does not match expectedLength')
}
return this.encodeChecked(concatArgs(versions, bytes))
}

encodeChecked(buffer: Buffer) {
const check = this.sha256(this.sha256(buffer)).slice(0, 4)
return this.encodeRaw(concatArgs(buffer, check))
}

encodeRaw(bytes: Buffer) {
return this.codec.encode(bytes)
}

/**
* Decoder.
*
* @param base58string Base58Check-encoded string to decode.
* @param opts Options object including the version byte(s) and the expected length of the data after decoding.
*/
decode(base58string: string, opts: {
versions?: (number | number[])[],
expectedLength?: number,
versionTypes?: string[]
} = {}) {
const versions = Array.isArray(opts.versions) ? opts.versions : [opts.versions]
const types = opts.versionTypes
if (versions) {
const withoutSum = this.decodeChecked(base58string)
const ret: {
version: number[] | null,
bytes: Buffer | null,
type: string | null // for seeds, 'ed25519' | 'secp256k1'
} = {
version: null,
bytes: null,
type: null
}
if (versions.length > 1 && !opts.expectedLength) {
throw new Error('expectedLength is required because there are >= 2 possible versions')
}
const versionLengthGuess = typeof versions[0] === 'number' ? 1 : (versions[0] as number[]).length
const payloadLength = opts.expectedLength || withoutSum.length - versionLengthGuess
const versionBytes = withoutSum.slice(0, -payloadLength)
const payload = withoutSum.slice(-payloadLength)

let foundVersion = false
for (let i = 0; i < versions.length; i++) {
const version: number[] = Array.isArray(versions[i]) ? versions[i] as number[] : [versions[i] as number]
if (seqEqual(versionBytes, version)) {
ret.version = version
ret.bytes = payload
if (types) {
ret.type = types[i]
}
foundVersion = true
}
}

if (!foundVersion) {
throw new Error('version_invalid: version bytes do not match any of the provided version(s)')
}

if (opts.expectedLength && ret.bytes.length !== opts.expectedLength) {
throw new Error('unexpected_payload_length: payload length does not match expectedLength')
}

return ret
}

// Assume that base58string is 'checked'
return this.decodeChecked(base58string)
}

decodeChecked(base58string: string) {
const buffer = this.decodeRaw(base58string)
if (buffer.length < 5) {
throw new Error('invalid_input_size: decoded data must have length >= 5')
}
if (!this.verifyCheckSum(buffer)) {
throw new Error('checksum_invalid')
}
return buffer.slice(0, -4)
}

decodeRaw(base58string: string) {
return this.codec.decode(base58string)
}

verifyCheckSum(bytes: Buffer) {
const computed = this.sha256(this.sha256(bytes.slice(0, -4))).slice(0, 4)
const checksum = bytes.slice(-4)
return seqEqual(computed, checksum)
}
}

/**
* XRP codec
*/

const createHash = require('create-hash')

const NODE_PUBLIC = 28
// const NODE_PRIVATE = 32
const ACCOUNT_ID = 0
const FAMILY_SEED = 0x21 // 33
const ED25519_SEED = [0x01, 0xE1, 0x4B] // [1, 225, 75]

const codecOptions = {
sha256: function(bytes: Uint8Array) {
return createHash('sha256').update(Buffer.from(bytes)).digest()
},
alphabet: 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz'
}

const codecWithXrpAlphabet = new Codec(codecOptions)

// entropy is an array (or Buffer?) of size 16
// type is 'ed25519' or 'secp256k1'
export function encodeSeed(entropy: number[], type: 'ed25519' | 'secp256k1'): string {
if (entropy.length !== 16) {
throw new Error('entropy must have length 16')
}
if (type !== 'ed25519' && type !== 'secp256k1') {
throw new Error('type must be ed25519 or secp256k1')
}
const opts = {
expectedLength: 16,

// for secp256k1, use `FAMILY_SEED`
versions: type === 'ed25519' ? ED25519_SEED : [FAMILY_SEED]
}

// prefixes entropy with version bytes
return codecWithXrpAlphabet.encode(entropy, opts)
}

export function decodeSeed(seed: string, opts: {
versionTypes?: string[],
versions?: (number | number[])[]
expectedLength?: number
} = {}) {
if (!opts.versionTypes || !opts.versions) {
opts.versionTypes = ['ed25519', 'secp256k1']
opts.versions = [ED25519_SEED, FAMILY_SEED]
}
if (!opts.expectedLength) {
opts.expectedLength = 16
}
return codecWithXrpAlphabet.decode(seed, opts)
}

export function encodeAccountID(bytes: number[]): string {
const opts = {versions: [ACCOUNT_ID], expectedLength: 20}
return codecWithXrpAlphabet.encode(bytes, opts)
}

export function decodeAccountID(accountId: string): Buffer {
const opts = {versions: [ACCOUNT_ID], expectedLength: 20}
return codecWithXrpAlphabet.decode(accountId, opts).bytes
}

export function decodeNodePublic(base58string: string): Buffer {
const opts = {versions: [NODE_PUBLIC], expectedLength: 33}
return codecWithXrpAlphabet.decode(base58string, opts).bytes
}

export function encodeNodePublic(bytes: number[]): string {
const opts = {versions: [NODE_PUBLIC], expectedLength: 33}
return codecWithXrpAlphabet.encode(bytes, opts)
}

// Address === AccountID
export function isValidAddress(address: string): boolean {
try {
this.decodeAccountID(address)
} catch (e) {
return false
}
return true
}
Loading

0 comments on commit a23cda3

Please sign in to comment.