Skip to content

Commit

Permalink
Re-add Ed25519PrivateKey.derive()
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Akhterov <akhterovd@gmail.com>
  • Loading branch information
janaakhterov committed Mar 19, 2020
1 parent b430dda commit b2c06fd
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 21 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@stablelib/hex": "^1.0.0",
"@stablelib/hmac": "^1.0.0",
"@stablelib/pbkdf2": "^1.0.0",
"@stablelib/sha256": "^1.0.0",
"@stablelib/sha384": "^1.0.0",
"@stablelib/sha512": "^1.0.0",
"@stablelib/utf8": "^1.0.0",
Expand Down
38 changes: 34 additions & 4 deletions src/crypto/Ed25519PrivateKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { Mnemonic } from "./Mnemonic";
import {
arraysEqual,
deriveChildKey,
deriveChildKey2,
ed25519PrivKeyPrefix,
pbkdf2,
randomBytes
} from "./util";
import { RawKeyPair } from "./RawKeyPair";
Expand All @@ -18,6 +18,7 @@ import { decodeDer } from "./der";
import * as base64 from "../encoding/base64";
import * as hex from "@stablelib/hex";
import { Hmac, HashAlgorithm } from "./Hmac";
import { Pbkdf2 } from "./Pbkdf2";

const beginPrivateKey = "-----BEGIN PRIVATE KEY-----\n";
const endPrivateKey = "-----END PRIVATE KEY-----\n";
Expand Down Expand Up @@ -129,7 +130,7 @@ export class Ed25519PrivateKey {
): Promise<Ed25519PrivateKey> {
const input = mnemonic.toString();
const salt = `mnemonic${passphrase}`;
const seed = await pbkdf2(input, salt, 2048, 64, "sha512");
const seed = await Pbkdf2.deriveKey(HashAlgorithm.Sha512, input, salt, 2048, 64);

const digest = await Hmac.hash(HashAlgorithm.Sha512, "ed25519 seed", seed);

Expand Down Expand Up @@ -178,16 +179,45 @@ export class Ed25519PrivateKey {
* an error.
*
* You can check if a key supports derivation with `.supportsDerivation`
*
* @deprecated `Ed25519PrivateKey.derive()` is deprecated and will eventually be replaced with the async variant `Ed25519PrivateKey.derive2()`
*/
public derive(index: number): Ed25519PrivateKey {
console.warn("`Ed25519PrivateKey.derive()` is deprecated and will eventually be replaced with the async variant `Ed25519PrivateKey.derive2()`");
if (this._chainCode == null) {
throw new Error("this Ed25519 private key does not support key derivation");
}

const {
keyBytes,
chainCode
} = deriveChildKey(this._keyData.subarray(0, 32), this._chainCode, index);

const key = Ed25519PrivateKey.fromBytes(keyBytes);
key._chainCode = chainCode;

return key;
}

/**
* Derive a new private key at the given wallet index.
*
* Only currently supported for keys created with `fromMnemonic()`; other keys will throw
* an error.
*
* You can check if a key supports derivation with `.supportsDerivation`
*
* Will eventually replace `Ed25519PrivateKey.derive()`
*/
public async derive(index: number): Promise<Ed25519PrivateKey> {
public async derive2(index: number): Promise<Ed25519PrivateKey> {
if (this._chainCode == null) {
throw new Error("this Ed25519 private key does not support key derivation");
}

const {
keyBytes,
chainCode
} = await deriveChildKey(this._keyData.subarray(0, 32), this._chainCode, index);
} = await deriveChildKey2(this._keyData.subarray(0, 32), this._chainCode, index);

const key = Ed25519PrivateKey.fromBytes(keyBytes);
key._chainCode = chainCode;
Expand Down
6 changes: 6 additions & 0 deletions src/crypto/Hmac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import * as utf8 from "@stablelib/utf8";
import { hmac } from "@stablelib/hmac";
import { SHA512 } from "@stablelib/sha512";
import { SHA384 } from "@stablelib/sha384";
import { SHA256 } from "@stablelib/sha256";

export enum HashAlgorithm {
Sha256 = "SHA-256",
Sha384 = "SHA-384",
Sha512 = "SHA-512"
}
Expand All @@ -28,6 +30,8 @@ export class Hmac {
return new Uint8Array(await window!.crypto.subtle.sign("HMAC", key, value));
} catch {
switch (algorithm) {
case HashAlgorithm.Sha256:
return hmac(SHA256, secretKey, value);
case HashAlgorithm.Sha384:
return hmac(SHA384, secretKey, value);
case HashAlgorithm.Sha512:
Expand All @@ -38,6 +42,8 @@ export class Hmac {
}

switch (algorithm) {
case HashAlgorithm.Sha256:
return crypto.createHmac("SHA256", value).update(secretKey).digest();
case HashAlgorithm.Sha384:
return crypto.createHmac("SHA384", value).update(secretKey).digest();
case HashAlgorithm.Sha512:
Expand Down
7 changes: 4 additions & 3 deletions src/crypto/Keystore.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as crypto from "crypto";
import * as nacl from "tweetnacl";
import { pbkdf2, randomBytes } from "./util";
import { randomBytes } from "./util";
import { RawKeyPair } from "./RawKeyPair";
import { KeyMismatchError } from "./KeyMismatchError";
import * as hex from "@stablelib/hex";
import { Hmac, HashAlgorithm } from "./Hmac";
import { Pbkdf2 } from "./Pbkdf2";

const AES_128_CTR = "aes-128-ctr";
const HMAC_SHA256 = "hmac-sha256";
Expand Down Expand Up @@ -46,7 +47,7 @@ export async function createKeystore(
const saltLen = 32;
const salt = await randomBytes(saltLen);

const key = await pbkdf2(passphrase, salt, c, dkLen, "sha256");
const key = await Pbkdf2.deriveKey(HashAlgorithm.Sha256, passphrase, salt, c, dkLen);

const iv = await randomBytes(16);

Expand Down Expand Up @@ -107,7 +108,7 @@ export async function loadKeystore(
const ivBytes = hex.decode(iv);
const cipherBytes = hex.decode(ciphertext);

const key = await pbkdf2(passphrase, saltBytes, c, dkLen, "sha256");
const key = await Pbkdf2.deriveKey(HashAlgorithm.Sha256, passphrase, saltBytes, c, dkLen);

const hmac = hex.decode(mac);
const verifyHmac = await Hmac.hash(HashAlgorithm.Sha384, key.slice(16), cipherBytes);
Expand Down
39 changes: 29 additions & 10 deletions src/crypto/Pbkdf2.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,65 @@
import { HashAlgorithm } from "./Hmac";
import { SHA256 } from "@stablelib/sha256";
import { SHA384 } from "@stablelib/sha384";
import { SHA512 } from "@stablelib/sha512";
import { pbkdf2 } from "./util";
import { deriveKey } from "@stablelib/pbkdf2";
import * as utf8 from "@stablelib/utf8";
import * as crypto from "crypto";
import { promisify } from "util";

export const pbkdf2 = promisify(crypto.pbkdf2);

export class Pbkdf2 {
public async deriveKey(
public static async deriveKey(
algorithm: HashAlgorithm,
password: Uint8Array,
salt: Uint8Array,
password: Uint8Array | string,
salt: Uint8Array | string,
iterations: number,
length: number
): Promise<Uint8Array> {
const pass = typeof password === "string" ?
// Valid ASCII is also valid UTF-8 so encoding the password as UTF-8
// should be fine if only valid ASCII characters are used in the password
utf8.encode(password) :
password;

const nacl = typeof salt === "string" ?
utf8.encode(salt) :
salt;

if (typeof window !== "undefined" && window != null) {
try {
const key = await window.crypto.subtle.importKey("raw", password, {
const key = await window.crypto.subtle.importKey("raw", pass, {
name: "PBKDF2",
hash: algorithm
}, false, [ "deriveBits" ]);

return new Uint8Array(await window.crypto.subtle.deriveBits({
name: "PBKDF2",
hash: algorithm,
salt,
salt: nacl,
iterations
}, key, length << 3));
} catch {
switch (algorithm) {
case HashAlgorithm.Sha256:
return deriveKey(SHA256, pass, nacl, iterations, length);
case HashAlgorithm.Sha384:
return deriveKey(SHA384, password, salt, iterations, length);
return deriveKey(SHA384, pass, nacl, iterations, length);
case HashAlgorithm.Sha512:
return deriveKey(SHA512, password, salt, iterations, length);
return deriveKey(SHA512, pass, nacl, iterations, length);
default: throw new Error("(BUG) Non-Exhaustive switch statement for algorithms");
}
}
}

switch (algorithm) {
case HashAlgorithm.Sha256:
return pbkdf2(password, nacl, iterations, length, "sha256");
case HashAlgorithm.Sha384:
return pbkdf2(password, salt, iterations, length, "sha384");
return pbkdf2(password, nacl, iterations, length, "sha384");
case HashAlgorithm.Sha512:
return pbkdf2(password, salt, iterations, length, "sha512");
return pbkdf2(password, nacl, iterations, length, "sha512");
default: throw new Error("(BUG) Non-Exhaustive switch statement for algorithms");
}
}
Expand Down
11 changes: 9 additions & 2 deletions src/crypto/pkcs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as crypto from "crypto";
import { AsnType, decodeDer } from "./der";
import { pbkdf2 } from "./util";
import { Pbkdf2 } from "./Pbkdf2";
import { HashAlgorithm } from "./Hmac";

export class AlgorithmIdentifier {
public readonly algIdent: string;
Expand Down Expand Up @@ -146,7 +147,13 @@ export class EncryptedPrivateKeyInfo {
const keyLen = pbkdf2Params.keyLength || 16;
const iv = pbes2Params.encScheme.parameters.bytes;

const key = await pbkdf2(passphrase, pbkdf2Params.salt, pbkdf2Params.iterCount, keyLen, "sha256");
const key = await Pbkdf2.deriveKey(
HashAlgorithm.Sha256,
passphrase,
pbkdf2Params.salt,
pbkdf2Params.iterCount,
keyLen
);

const cipher = crypto.createDecipheriv("aes-128-cbc", key, iv);
const decrypted = Buffer.concat([ cipher.update(this.data), cipher[ "final" ]() ]);
Expand Down
26 changes: 24 additions & 2 deletions src/crypto/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as crypto from "crypto";
import { promisify } from "util";
import { Hmac, HashAlgorithm } from "./Hmac";

export const pbkdf2 = promisify(crypto.pbkdf2);
export const randomBytes = promisify(crypto.randomBytes);

// we could go through the whole BS of producing a DER-encoded structure but it's quite simple
Expand All @@ -14,7 +13,7 @@ export const ed25519PubKeyPrefix = "302a300506032b6570032100";
export const hmacAlgo = "sha384";

/** SLIP-10/BIP-32 child key derivation */
export async function deriveChildKey(
export async function deriveChildKey2(
parentKey: Uint8Array,
chainCode: Uint8Array,
index: number
Expand All @@ -33,6 +32,29 @@ export async function deriveChildKey(
return { keyBytes: digest.subarray(0, 32), chainCode: digest.subarray(32) };
}

/** SLIP-10/BIP-32 child key derivation */
export function deriveChildKey(
parentKey: Uint8Array,
chainCode: Uint8Array,
index: number
): { keyBytes: Uint8Array; chainCode: Uint8Array } {
// webpack version of crypto complains if input types are not `Buffer`
const hmac = crypto.createHmac("SHA512", Buffer.from(chainCode));
const input = new Uint8Array(37);
// 0x00 + parentKey + index(BE)
input[ 0 ] = 0;
input.set(parentKey, 1);
new DataView(input.buffer).setUint32(33, index, false);
// set the index to hardened
input[ 33 ] |= 128;

hmac.update(input);

const digest = hmac.digest();

return { keyBytes: digest.subarray(0, 32), chainCode: digest.subarray(32) };
}

export function arraysEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a === b) return true;
if (a == null || b == null) return false;
Expand Down
9 changes: 9 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,15 @@
"@stablelib/hmac" "^1.0.0"
"@stablelib/wipe" "^1.0.0"

"@stablelib/sha256@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@stablelib/sha256/-/sha256-1.0.0.tgz#05fc33814308caedab30068bd26bd31424c502e7"
integrity sha512-+IEzCXO6HSyYWV+5TqdFjcUYgkebdiadzRtMXJg6ia68WQm2xHpABl5t0vVdtvgTlw7matBRhImunAHUFIAEUg==
dependencies:
"@stablelib/binary" "^1.0.0"
"@stablelib/hash" "^1.0.0"
"@stablelib/wipe" "^1.0.0"

"@stablelib/sha384@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@stablelib/sha384/-/sha384-1.0.0.tgz#c5da24396f123a4874900fb020d2be871ba1d804"
Expand Down

0 comments on commit b2c06fd

Please sign in to comment.