Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor key derivation #1309

Merged
merged 17 commits into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- `TRANSACTION_HAS_UNKNOWN_FIELDS` and `ACCOUNT_IS_IMMUTABLE` in `Status`
- `toStandard[Ed25519|ECDSAsecp256k1]PrivateKey()` to `Mnemonic`
- `fromSeed[ED25519|ECDSAsecp256k1]()` to `PrivateKey`
- `[PrivateKeyED25519|PrivateKeyECDSA].fromSeed()`
- `Bip32Utils` class

### Fixed
- Misleading logging when an unhealthy node is hit
- ECDSA secp256k1 keys now support derivation

### Deprecated
- `Mnemonic.toPrivateKey()` use `Mnemonic.toStandard[Ed25519|ECDSAsecp256k1]PrivateKey` instead
- `PrivateKey.fromMnemonic()` use `Mnemonic.toStandard[Ed25519|ECDSAsecp256k1]PrivateKey` instead

## 2.19.0

Expand Down
14 changes: 2 additions & 12 deletions examples/src/main/java/GenerateKeyWithMnemonicExample.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,11 @@ private GenerateKeyWithMnemonicExample() {

public static void main(String[] args) {
Mnemonic mnemonic = Mnemonic.generate24();
PrivateKey privateKey;
try {
privateKey = mnemonic.toPrivateKey();
} catch (BadMnemonicException e) {
throw new Error(e.reason.toString());
}
PrivateKey privateKey = mnemonic.toStandardEd25519PrivateKey("", 0);
PublicKey publicKey = privateKey.getPublicKey();

Mnemonic mnemonic12 = Mnemonic.generate12();
PrivateKey privateKey12;
try {
privateKey12 = mnemonic12.toPrivateKey();
} catch (BadMnemonicException e) {
throw new Error(e.reason.toString());
}
PrivateKey privateKey12 = mnemonic12.toStandardEd25519PrivateKey("", 0);
PublicKey publicKey12 = privateKey12.getPublicKey();

System.out.println("mnemonic 24 word = " + mnemonic);
Expand Down
74 changes: 53 additions & 21 deletions sdk/src/main/java/com/hedera/hashgraph/sdk/Mnemonic.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import com.google.common.base.Joiner;
import com.google.errorprone.annotations.Var;
import com.hedera.hashgraph.sdk.utils.Bip32Utils;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
Expand Down Expand Up @@ -62,17 +63,11 @@ public final class Mnemonic {
*/
public final List<CharSequence> words;

public boolean isLegacy = false;

@Nullable
private String asString;

@SuppressWarnings("StaticAssignmentInConstructor")
private Mnemonic(List<? extends CharSequence> words) {
if (words.size() == 22) {
isLegacy = true;
}

this.words = Collections.unmodifiableList(words);
}

Expand Down Expand Up @@ -317,24 +312,19 @@ private static boolean[] bytesToBits(byte[] dat) {
}

/**
* @deprecated use {@link #toStandardEd25519PrivateKey(String, int)} ()} or {@link #toStandardECDSAsecp256k1PrivateKey(String, int)} (String, int)} instead
* Recover a private key from this mnemonic phrase.
* <p>
* This is not compatible with the phrases generated by the Android and iOS wallets;
* use the no-passphrase version instead.
*
* @param passphrase the passphrase used to protect the mnemonic (not used in the
* mobile wallets, use {@link #toPrivateKey()} instead.)
* @param passphrase the passphrase used to protect the mnemonic
* @return the recovered key; use {@link PrivateKey#derive(int)} to get a
* key for an account index (0 for default account)
* @see PrivateKey#fromMnemonic(Mnemonic, String)
*/
public PrivateKey toPrivateKey(String passphrase) throws BadMnemonicException {
if (isLegacy) {
if (passphrase.compareTo("") != 0) {
throw new Error("Legacy mnemonic doesn't support passphrases");
}
return this.toLegacyPrivateKey();
}
@Deprecated
public PrivateKey toPrivateKey(String passphrase) {
return PrivateKey.fromMnemonic(this, passphrase);
}

Expand All @@ -353,6 +343,7 @@ public PrivateKey toLegacyPrivateKey() throws BadMnemonicException {
}

/**
* @deprecated use {@link #toStandardEd25519PrivateKey(String, int)} ()} or {@link #toStandardECDSAsecp256k1PrivateKey(String, int)} (String, int)} instead
* Recover a private key from this mnemonic phrase.
*
* @return the recovered key; use
Expand All @@ -361,7 +352,8 @@ public PrivateKey toLegacyPrivateKey() throws BadMnemonicException {
* default account)
* @see PrivateKey#fromMnemonic(Mnemonic)
*/
public PrivateKey toPrivateKey() throws BadMnemonicException {
@Deprecated
public PrivateKey toPrivateKey() {
return toPrivateKey("");
}

Expand Down Expand Up @@ -448,7 +440,7 @@ private byte[] wordsToEntropyAndChecksum() {
@Var int scratch = 0;
@Var int offset = 0;
for (CharSequence word : words) {
int index = getWordIndex(word, isLegacy);
int index = getWordIndex(word, false);

if (index < 0) {
// should also be checked in `validate()`
Expand Down Expand Up @@ -476,10 +468,6 @@ private byte[] wordsToEntropyAndChecksum() {
}

private byte[] wordsToLegacyEntropy() throws BadMnemonicException {
if (!isLegacy) {
throw new BadMnemonicException(this, BadMnemonicReason.NotLegacy);
}

var indices = new int[words.size()];
for (var i = 0; i < words.size(); i++) {
indices[i] = getWordIndex(words.get(i), true);
Expand Down Expand Up @@ -554,4 +542,48 @@ private byte[] wordsToLegacyEntropy2() throws BadMnemonicException {

return entropy;
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it would make sense to have versions of these toStandard functions with default passphrase and index

  • toStandard(passphrase) (using index 0)
  • toStandard() (using no passphrase and index 0)
  • toStandard(index) (using no passphrase) << this one might be overkill, though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We thought about having multiple versions of the toStandard methods but we decided it's better to have only one, because it cannot be done in the other SDKs. JS and Go don't support overloading of methods.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've fine with that logic, but this really needs to be folded into the standard proposal.

For example, I know the C++ SDK effort has already implemented the proposal as it currently exists, with default parameters.

/**
* Recover an Ed25519 private key from this mnemonic phrase, with an
* optional passphrase.
*
* @param passphrase the passphrase used to protect the mnemonic
* @param index the derivation index
* @return the private key
*/
public PrivateKey toStandardEd25519PrivateKey(String passphrase, int index) {
var seed = this.toSeed(passphrase);
PrivateKey derivedKey = PrivateKey.fromSeedED25519(seed);

for (int i : new int[]{44, 3030, 0, 0, index}) {
derivedKey = derivedKey.derive(i);
}

return derivedKey;
}

/**
* Recover an ECDSAsecp256k1 private key from this mnemonic phrase, with an
* optional passphrase.
*
* @param passphrase the passphrase used to protect the mnemonic
* @param index the derivation index
* @return the private key
*/
public PrivateKey toStandardECDSAsecp256k1PrivateKey(String passphrase, int index) {
var seed = this.toSeed(passphrase);
PrivateKey derivedKey = PrivateKey.fromSeedECDSAsecp256k1(seed);

// Harden the first 3 indexes
for (int i : new int[]{
Bip32Utils.toHardenedIndex(44),
Bip32Utils.toHardenedIndex(3030),
Bip32Utils.toHardenedIndex(0),
0,
index}) {
derivedKey = derivedKey.derive(i);
}

return derivedKey;
}
}
39 changes: 27 additions & 12 deletions sdk/src/main/java/com/hedera/hashgraph/sdk/PrivateKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
import com.hedera.hashgraph.sdk.proto.SignedTransaction;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.math.ec.rfc8032.Ed25519;
import org.bouncycastle.util.encoders.Hex;
Expand All @@ -34,7 +32,6 @@
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/**
Expand Down Expand Up @@ -75,6 +72,27 @@ public static PrivateKey generateECDSA() {
}

/**
* Extract the ED25519 private key from a seed.
*
* @param seed the seed
* @return the ED25519 private key
*/
public static PrivateKey fromSeedED25519(byte[] seed) {
return PrivateKeyED25519.fromSeed(seed);
}

/**
* Extract the ECDSA private key from a seed.
*
* @param seed the seed
* @return the ECDSA private key
*/
public static PrivateKey fromSeedECDSAsecp256k1(byte[] seed) {
return PrivateKeyECDSA.fromSeed(seed);
}

/**
* @deprecated use {@link Mnemonic#toStandardEd25519PrivateKey(String, int)} ()} or {@link Mnemonic#toStandardECDSAsecp256k1PrivateKey(String, int)} (String, int)} instead
* Recover a private key from a generated mnemonic phrase and a passphrase.
* <p>
* This is not compatible with the phrases generated by the Android and iOS wallets;
Expand All @@ -86,17 +104,10 @@ public static PrivateKey generateECDSA() {
* @return the recovered key; use {@link #derive(int)} to get a key for an account index (0
* for default account)
*/
@Deprecated
public static PrivateKey fromMnemonic(Mnemonic mnemonic, String passphrase) {
var seed = mnemonic.toSeed(passphrase);

var hmacSha512 = new HMac(new SHA512Digest());
hmacSha512.init(new KeyParameter("ed25519 seed".getBytes(StandardCharsets.UTF_8)));
hmacSha512.update(seed, 0, seed.length);

var derivedState = new byte[hmacSha512.getMacSize()];
hmacSha512.doFinal(derivedState, 0);

@Var PrivateKey derivedKey = PrivateKeyED25519.derivableKeyED25519(derivedState);
@Var PrivateKey derivedKey = fromSeedED25519(seed);

// BIP-44 path with the Hedera Hbar coin-type (omitting key index)
// we pre-derive most of the path as the mobile wallets don't expose more than the index
Expand All @@ -110,6 +121,7 @@ public static PrivateKey fromMnemonic(Mnemonic mnemonic, String passphrase) {
}

/**
* @deprecated use {@link Mnemonic#toStandardEd25519PrivateKey(String, int)} ()} or {@link Mnemonic#toStandardECDSAsecp256k1PrivateKey(String, int)} (String, int)} instead
* Recover a private key from a mnemonic phrase compatible with the iOS and Android wallets.
* <p>
* An overload of {@link #fromMnemonic(Mnemonic, String)} which uses an empty string for the
Expand All @@ -119,6 +131,7 @@ public static PrivateKey fromMnemonic(Mnemonic mnemonic, String passphrase) {
* @return the recovered key; use {@link #derive(int)} to get a key for an account index (0
* for default account)
*/
@Deprecated
public static PrivateKey fromMnemonic(Mnemonic mnemonic) {
return fromMnemonic(mnemonic, "");
}
Expand Down Expand Up @@ -449,4 +462,6 @@ com.hedera.hashgraph.sdk.proto.Key toProtobufKey() {
* @return are we an ECDSA key
*/
public abstract boolean isECDSA();

public abstract KeyParameter getChainCode();
}
Loading