Introduce key suri (path derivation) (#358)

* Introduce key suri (path derivation)

* Errant console.log

* let -> const

* Add seed derivation from path

* Allow new-generation derivation

* Add addFromUri to keyring

* Use addFromUri in testing keyring

* Add suri tests

* Prepare for sr25519 derivation

* CHANGELOG entry

* ed25519 is aligned with substrate

* Bump version
This commit is contained in:
Jaco Greeff
2019-03-13 14:39:10 +01:00
committed by GitHub
parent a1e56cfb81
commit f1ca4eef0e
37 changed files with 564 additions and 120 deletions
+1
View File
@@ -6,6 +6,7 @@ import { schnorrkelWaitReady } from './schnorrkel';
export * from './blake2';
export * from './keccak';
export * from './key';
export * from './mnemonic';
export * from './nacl';
export * from './random';
@@ -0,0 +1,85 @@
// Copyright 2017-2019 @polkadot/util-crypto authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import BN from 'bn.js';
import { bnToHex, compactAddLength, hexToU8a, isBn, isHex, isNumber, isString, stringToU8a } from '@polkadot/util/index';
import blake2AsU8a from '../blake2/asU8a';
const RE_NUMBER = /^\d+$/;
const JUNCTION_ID_LEN: number = 32;
const BN_OPTIONS = {
bitLength: 256,
isLe: true
};
export default class DeriveJunction {
private _chainCode: Uint8Array = new Uint8Array(32);
private _isHard: boolean = false;
static from (value: string): DeriveJunction {
const [code, isHard] = value[0] === '/'
? [value.substr(1), true]
: [value, false];
const result = new DeriveJunction();
result.soft(
RE_NUMBER.test(code)
? parseInt(code, 10)
: code
);
return isHard
? result.harden()
: result;
}
get chainCode (): Uint8Array {
return this._chainCode;
}
get isHard (): boolean {
return this._isHard;
}
get isSoft (): boolean {
return !this._isHard;
}
hard (value: number | BN | string | Uint8Array): DeriveJunction {
return this.soft(value).harden();
}
harden (): DeriveJunction {
this._isHard = true;
return this;
}
soft (value: number | BN | string | Uint8Array): DeriveJunction {
if (isNumber(value) || isBn(value)) {
return this.soft(bnToHex(value, BN_OPTIONS));
} else if (isString(value)) {
return isHex(value)
? this.soft(hexToU8a(value))
: this.soft(compactAddLength(stringToU8a(value)));
}
if (value.length > JUNCTION_ID_LEN) {
return this.soft(blake2AsU8a(value));
}
this._chainCode.fill(0);
this._chainCode.set(value, 0);
return this;
}
soften (): DeriveJunction {
this._isHard = false;
return this;
}
}
@@ -0,0 +1,131 @@
// Copyright 2017-2019 @polkadot/util-crypto authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import keyExtract from './extract';
describe('keyExtract', () => {
it('derives on "hello world"', () => {
expect(
keyExtract('hello world')
).toEqual({
path: [],
phrase: 'hello world'
});
});
it('derives on "hello world/1', () => {
expect(
keyExtract('hello world/1')
).toEqual({
path: [
{ _isHard: false, _chainCode: Uint8Array.from([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) }
],
phrase: 'hello world'
});
});
it('derives on "hello world/DOT', () => {
expect(
keyExtract('hello world/DOT')
).toEqual({
path: [
{ _isHard: false, _chainCode: Uint8Array.from([12, 68, 79, 84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) }
],
phrase: 'hello world'
});
});
it('derives on "hello world//1', () => {
expect(
keyExtract('hello world//1')
).toEqual({
path: [
{ _isHard: true, _chainCode: Uint8Array.from([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) }
],
phrase: 'hello world'
});
});
it('derives on "hello world//DOT', () => {
expect(
keyExtract('hello world//DOT')
).toEqual({
path: [
{ _isHard: true, _chainCode: Uint8Array.from([12, 68, 79, 84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) }
],
phrase: 'hello world'
});
});
it('derives on "hello world//1/DOT', () => {
expect(
keyExtract('hello world//1/DOT')
).toEqual({
path: [
{ _isHard: true, _chainCode: Uint8Array.from([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) },
{ _isHard: false, _chainCode: Uint8Array.from([12, 68, 79, 84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) }
],
phrase: 'hello world'
});
});
it('derives on "hello world//DOT/1', () => {
expect(
keyExtract('hello world//DOT/1')
).toEqual({
path: [
{ _isHard: true, _chainCode: Uint8Array.from([12, 68, 79, 84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) },
{ _isHard: false, _chainCode: Uint8Array.from([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) }
],
phrase: 'hello world'
});
});
it('derives on "hello world///password"', () => {
expect(
keyExtract('hello world///password')
).toEqual({
password: 'password',
path: [],
phrase: 'hello world'
});
});
it('derives on "hello world//1/DOT///password"', () => {
expect(
keyExtract('hello world//1/DOT///password')
).toEqual({
path: [
{ _isHard: true, _chainCode: Uint8Array.from([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) },
{ _isHard: false, _chainCode: Uint8Array.from([12, 68, 79, 84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) }
],
password: 'password',
phrase: 'hello world'
});
});
it('derives on "hello world/1//DOT///password"', () => {
expect(
keyExtract('hello world/1//DOT///password')
).toEqual({
path: [
{ _isHard: false, _chainCode: Uint8Array.from([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) },
{ _isHard: true, _chainCode: Uint8Array.from([12, 68, 79, 84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) }
],
password: 'password',
phrase: 'hello world'
});
});
it('derives on actual Alice', () => {
expect(
keyExtract('bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice')
).toEqual({
path: [
{ _isHard: true, _chainCode: Uint8Array.from([20, 65, 108, 105, 99, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) }
],
phrase: 'bottom drive obey lake curtain smoke basket hold race lonely fit walk'
});
});
});
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2017-2019 @polkadot/util-crypto authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { assert, isNull } from '@polkadot/util/index';
import DeriveJunction from './DeriveJunction';
type DeriveResult = {
password?: string,
path: Array<DeriveJunction>,
phrase: string
};
const RE_CAPTURE = /^(\w+( \w+)*)((\/\/?[^\/]+)*)(\/\/\/(.*))?$/;
const RE_JUNCTION = /\/(\/?)([^\/]+)/g;
/**
* @description Extracts the phrase, path and password from a SURI format for specifying secret keys `<secret>/<soft-key>//<hard-key>///<password>` (the `///password` may be omitted, and `/<soft-key>` and `//<hard-key>` maybe repeated and mixed).
*/
export default function keyExtract (suri: string): DeriveResult {
const matches = suri.match(RE_CAPTURE);
assert(!isNull(matches), `Unable to match '${suri}' to a secret URI`);
const [, phrase, , junctions, , , password] = matches as Array<any>;
const parts = junctions.match(RE_JUNCTION);
const path: Array<DeriveJunction> = [];
if (parts) {
parts.forEach((value: string) => {
path.push(DeriveJunction.from(value.substr(1)));
});
}
return {
password,
path,
phrase
};
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2017-2019 @polkadot/util-crypto authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeypairType } from '../types';
import DeriveJunction from './DeriveJunction';
import keyHdkdEd15519 from './hdkdEd25519';
import keyHdkdSr15519 from './hdkdSr25519';
export default function keyFromPath (seed: Uint8Array, path: Array<DeriveJunction>, type: KeypairType): Uint8Array {
return path.reduce((seed, junction) => {
return type === 'ed25519'
? keyHdkdEd15519(seed, junction)
: keyHdkdSr15519(seed, junction);
}, seed);
}
@@ -0,0 +1,18 @@
// Copyright 2017-2019 @polkadot/util-crypto authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { assert, compactAddLength, stringToU8a, u8aConcat } from '@polkadot/util/index';
import blake2AsU8a from '../blake2/asU8a';
import DeriveJunction from './DeriveJunction';
const HDKD = compactAddLength(stringToU8a('Ed25519HDKD'));
export default function keyHdkdEd25519 (seed: Uint8Array, { chainCode, isHard }: DeriveJunction): Uint8Array {
assert(isHard, 'A soft key was found in the path (and is unsupported)');
return blake2AsU8a(
u8aConcat(HDKD, seed, chainCode)
);
}
@@ -0,0 +1,17 @@
// Copyright 2017-2019 @polkadot/util-crypto authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { stringToU8a, u8aConcat } from '@polkadot/util/index';
import blake2AsU8a from '../blake2/asU8a';
import DeriveJunction from './DeriveJunction';
const HDKD = stringToU8a('SchnorrRistrettoHDKD');
// FIXME This should pull in from schnorrkel and do the magic there
export default function keyHdkdSr25519 (seed: Uint8Array, { chainCode }: DeriveJunction): Uint8Array {
return blake2AsU8a(
u8aConcat(HDKD, seed, chainCode)
);
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2017-2019 @polkadot/util-crypto authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
/**
* @summary Create keys from paths, seeds and password
*/
export { default as keyExtract } from './extract';
export { default as keyFromPath } from './fromPath';
export { default as keyHdkdEd25519 } from './hdkdEd25519';
export { default as keyHdkdSr25519 } from './hdkdEd25519';
@@ -2,12 +2,12 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeypairType } from '../../types';
import { Keypair } from '../../types';
import { naclKeypairFromRandom } from '../index';
describe('naclKeypairFromRandom', () => {
let keypair: KeypairType;
let keypair: Keypair;
beforeEach(() => {
keypair = naclKeypairFromRandom();
@@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeypairType } from '../../types';
import { Keypair } from '../../types';
import nacl from 'tweetnacl';
@@ -20,6 +20,6 @@ import nacl from 'tweetnacl';
* naclKeypairFromRandom(); // => { secretKey: [...], publicKey: [...] }
* ```
*/
export default function naclKeypairFromRandom (): KeypairType {
export default function naclKeypairFromRandom (): Keypair {
return nacl.sign.keyPair();
}
@@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeypairType } from '../../types';
import { Keypair } from '../../types';
import nacl from 'tweetnacl';
@@ -20,6 +20,6 @@ import nacl from 'tweetnacl';
* naclKeypairFromSecret(...); // => { secretKey: [...], publicKey: [...] }
* ```
*/
export default function naclKeypairFromSecret (secret: Uint8Array): KeypairType {
export default function naclKeypairFromSecret (secret: Uint8Array): Keypair {
return nacl.sign.keyPair.fromSecretKey(secret);
}
@@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeypairType } from '../../types';
import { Keypair } from '../../types';
import nacl from 'tweetnacl';
@@ -20,6 +20,6 @@ import nacl from 'tweetnacl';
* naclKeypairFromSeed(...); // => { secretKey: [...], publicKey: [...] }
* ```
*/
export default function naclKeypairFromSeed (seed: Uint8Array): KeypairType {
export default function naclKeypairFromSeed (seed: Uint8Array): Keypair {
return nacl.sign.keyPair.fromSeed(seed);
}
@@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeypairType } from '../../types';
import { Keypair } from '../../types';
import { stringToU8a } from '@polkadot/util/index';
@@ -23,7 +23,7 @@ import fromSeed from './fromSeed';
* naclKeypairFromString('test'); // => { secretKey: [...], publicKey: [...] }
* ```
*/
export default function naclKeypairFromString (value: string): KeypairType {
export default function naclKeypairFromString (value: string): Keypair {
return fromSeed(
blake2AsU8a(
stringToU8a(value),
+2 -2
View File
@@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeypairType } from '../types';
import { Keypair } from '../types';
import nacl from 'tweetnacl';
import { assert } from '@polkadot/util/index';
@@ -21,7 +21,7 @@ import { assert } from '@polkadot/util/index';
* naclSign([...], [...]); // => [...]
* ```
*/
export default function naclSign (message: Uint8Array, { secretKey }: Partial<KeypairType>): Uint8Array {
export default function naclSign (message: Uint8Array, { secretKey }: Partial<Keypair>): Uint8Array {
assert(secretKey && secretKey.length === 64, 'Expected valid secretKey, 64 bytes');
return nacl.sign.detached(message, secretKey as Uint8Array);
@@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeypairType } from '../../types';
import { Keypair } from '../../types';
import schnorrkel from '@polkadot/schnorrkel-js';
@@ -13,7 +13,7 @@ const PUB_LEN = 32;
* @name schnorrkelKeypairFromSeed
* @description Returns a object containing a `publicKey` & `secretKey` generated from the supplied seed.
*/
export default function schnorrkelKeypairFromSeed (seed: Uint8Array): KeypairType {
export default function schnorrkelKeypairFromSeed (seed: Uint8Array): Keypair {
const full = schnorrkel.keypair_from_seed(seed);
return {
+2 -2
View File
@@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeypairType } from '../types';
import { Keypair } from '../types';
import { assert } from '@polkadot/util/index';
@@ -12,7 +12,7 @@ import schnorrkel from '@polkadot/schnorrkel-js';
* @name schnorrkelSign
* @description Returns message signature of `message`, using the supplied pair
*/
export default function schnorrkelSign (message: Uint8Array, { publicKey, secretKey }: Partial<KeypairType>): Uint8Array {
export default function schnorrkelSign (message: Uint8Array, { publicKey, secretKey }: Partial<Keypair>): Uint8Array {
assert(publicKey && publicKey.length === 32, 'Expected valid publicKey, 32-bytes');
assert(secretKey && secretKey.length === 64, 'Expected valid secretKey, 64-bytes');
+3 -1
View File
@@ -2,7 +2,9 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
export type KeypairType = {
export type Keypair = {
publicKey: Uint8Array;
secretKey: Uint8Array;
};
export type KeypairType = 'ed25519' | 'sr25519';