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:
@@ -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'
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -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,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,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');
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user