Skip to content

Commit

Permalink
BIP39 CLI support WIP, loosen restrictions on converting blobs to
Browse files Browse the repository at this point in the history
addresses
  • Loading branch information
mikera committed Jan 18, 2024
1 parent 5f28557 commit 50b685b
Show file tree
Hide file tree
Showing 13 changed files with 141 additions and 50 deletions.
22 changes: 15 additions & 7 deletions convex-cli/src/main/java/convex/cli/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ public char[] getStorePassword() {
} else {
if (!nonInteractive) {
Console console = System.console();
storepass= console.readPassword("Keystore Password: ");
storepass= console.readPassword("Enter Keystore Password: ");
}

if (storepass==null) {
Expand Down Expand Up @@ -285,18 +285,26 @@ public String getKeyStoreFilename() {
*/
public KeyStore getKeystore() {
if (keyStore==null) {
keyStore=loadKeyStore(false);
keyStore=loadKeyStore(false,getStorePassword());
}
return keyStore;
}

/**
* Loads the currently configured get Store
* Loads the currently configured key Store
* @param isCreate Flag to indicate if keystore should be created if absent
* @return KeyStore instance, or null if does not exist
*/
public KeyStore loadKeyStore(boolean isCreate) {
char[] password=getStorePassword();
public KeyStore loadKeyStore() {
return loadKeyStore(false,getStorePassword());
}

/**
* Loads the currently configured key Store
* @param isCreate Flag to indicate if keystore should be created if absent
* @return KeyStore instance, or null if does not exist
*/
public KeyStore loadKeyStore(boolean isCreate, char[] password) {
File keyFile = new File(getKeyStoreFilename());
try {
if (keyFile.exists()) {
Expand Down Expand Up @@ -406,11 +414,11 @@ public Convex connect() {
throw new TODOException();
}

public void saveKeyStore() {
public void saveKeyStore(char[] storePassword) {
// save the keystore file
if (keyStore==null) throw new CLIError("Trying to save a keystore that has not been loaded!");
try {
PFXTools.saveStore(keyStore, new File(getKeyStoreFilename()), getStorePassword());
PFXTools.saveStore(keyStore, new File(getKeyStoreFilename()), storePassword);
} catch (Throwable t) {
throw Utils.sneakyThrow(t);
}
Expand Down
69 changes: 54 additions & 15 deletions convex-cli/src/main/java/convex/cli/key/KeyGenerate.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package convex.cli.key;

import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import convex.cli.Constants;
import convex.cli.Main;
import convex.core.crypto.AKeyPair;
import convex.core.crypto.BIP39;
import convex.core.crypto.PFXTools;
import convex.core.data.Blob;
import convex.core.util.Utils;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.Option;


/**
Expand All @@ -25,44 +30,78 @@
@Command(name="generate",
aliases={"gen"},
mixinStandardHelpOptions=true,
description="Generate private key pairs in the currently configured keystore. Will create a keystore if it does not exist.")
description="Generate private key pair(s) in the currently configured keystore. Will create a keystore if it does not exist.")
public class KeyGenerate extends AKeyCommand {

private static final Logger log = LoggerFactory.getLogger(KeyGenerate.class);

@Parameters(paramLabel="count",
@Option(names="--count",
defaultValue="" + Constants.KEY_GENERATE_COUNT,
description="Number of keys to generate. Default: ${DEFAULT-VALUE}")
private int count;

@Option(names="--bip39",
description="Generate BIP39 mnemonic seed phrases and passphrase")
private boolean bip39;

@Option(names="--passphrase",
description="BIP39 optional passphrase")
private String passphrase;


private AKeyPair generateKeyPair() {
try {
if (bip39) {
String mnemonic=BIP39.createSecureRandom(12);
cli().println(mnemonic);
if (cli().isInteractive()) {
passphrase=new String(System.console().readPassword("Enter BIP39 passphrase: "));
} else {
if (passphrase==null) passphrase="";
}
Blob bipseed;
bipseed = BIP39.getSeed(mnemonic, passphrase);
AKeyPair result= BIP39.seedToKeyPair(bipseed);
return result;
} else {
return AKeyPair.generate();
}
} catch (GeneralSecurityException e) {
throw Utils.sneakyThrow(e);
}
}

@Override
public void run() {
// sub command to generate keys
Main mainParent = cli();

// check the number of keys to generate.
if (count < 0) {
log.warn("Unlikely count of keys to generate: "+count);
count=0;
if (count <= 0) {
log.warn("No keys to generate: count = "+count);
return;
}
log.debug("Generating {} keys",count);

char[] storePass=cli().getStorePassword();
try {
KeyStore ks=cli().loadKeyStore(true);
char[] keyPassword=mainParent.getKeyPassword();
KeyStore ks=cli().loadKeyStore(true,storePass);
for ( int index = 0; index < count; index ++) {
AKeyPair kp=AKeyPair.generate();
AKeyPair kp=generateKeyPair();
String publicKeyHexString = kp.getAccountKey().toHexString();
mainParent.println(publicKeyHexString); // Output generated public key
cli().println(publicKeyHexString); // Output generated public key
char[] keyPassword=cli().getKeyPassword();
PFXTools.setKeyPair(ks, kp, keyPassword);
Arrays.fill(keyPassword, 'p');
}
log.debug(count+ " keys successfully generated");
cli().saveKeyStore();
cli().saveKeyStore(storePass);
log.trace("Keystore saved successfully");
} catch (Throwable e) {
throw Utils.sneakyThrow(e);
} finally {
Arrays.fill(storePass,'z');
}
}




}
30 changes: 14 additions & 16 deletions convex-cli/src/main/java/convex/cli/key/KeyImport.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import org.slf4j.LoggerFactory;

import convex.cli.CLIError;
import convex.cli.Main;
import convex.core.crypto.AKeyPair;
import convex.core.crypto.PEMTools;
import picocli.CommandLine.Command;
Expand All @@ -37,21 +36,20 @@ public class KeyImport extends AKeyCommand {
@ParentCommand
protected Key keyParent;

@Option(names={"-i", "--import-text"},
description="Import format PEM text of the keypair.")
private String importText;

@Option(names={"-f", "--import-file"},
description="Import file name of the keypair file.")
@Option(names={"-i", "--import-file"},
description="Import file for the the keypair.")
private String importFilename;

@Option(names={"--pem-text"},
description="PEM format text to import.")
private String importText;

@Option(names={"--import-password"},
description="Password of the imported key.")
@Option(names={"--pem-password"},
description="Password of the imported PEM key.")
private String importPassword;

@Override
public void run() {
Main mainParent = cli();
if (importFilename != null && importFilename.length() > 0) {
Path path=Paths.get(importFilename);
try {
Expand All @@ -64,8 +62,7 @@ public void run() {
}
}
if (importText == null || importText.length() == 0) {
log.warn("You need to provide an import text '--import' or import filename '--import-file' to import a private key");
return;
throw new CLIError("You need to provide '--pem-text' or import filename '--import-file' to import a private key");
}

if (importPassword == null || importPassword.length() == 0) {
Expand All @@ -75,11 +72,12 @@ public void run() {
PrivateKey privateKey = PEMTools.decryptPrivateKeyFromPEM(importText, importPassword.toCharArray());
AKeyPair keyPair = AKeyPair.create(privateKey);

char[] keyPassword=mainParent.getKeyPassword();
mainParent.addKeyPairToStore(keyPair,keyPassword);
char[] storePassword=cli().getStorePassword();
char[] keyPassword=cli().getKeyPassword();
cli().addKeyPairToStore(keyPair,keyPassword);
Arrays.fill(keyPassword, 'x');
cli().saveKeyStore(storePassword);

cli().saveKeyStore();
mainParent.println(keyPair.getAccountKey().toHexString());
cli().println(keyPair.getAccountKey().toHexString());
}
}
3 changes: 2 additions & 1 deletion convex-cli/src/main/java/convex/cli/key/KeyList.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ public class KeyList extends AKeyCommand {

@Override
public void run() {
KeyStore keyStore = cli().loadKeyStore(false);
KeyStore keyStore = cli().loadKeyStore();
if (keyStore==null) throw new CLIError("Keystore does not exist. Specify a valid keystore or use `convex key gen` to create one.");

Enumeration<String> aliases;
try {
aliases = keyStore.aliases();
Expand Down
2 changes: 1 addition & 1 deletion convex-cli/src/main/java/convex/cli/peer/PeerCreate.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public void run() {

try {
// create a keystore if it does not exist
keyStore = mainParent.loadKeyStore(true);
keyStore = mainParent.loadKeyStore();
} catch (Error e) {
log.info(e.getMessage());
return;
Expand Down
4 changes: 2 additions & 2 deletions convex-cli/src/test/java/convex/cli/key/KeyImportTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ public void testKeyImport() {
"-n",
"--keystore-password", new String(KEYSTORE_PASSWORD),
"--keystore", KEYSTORE_FILENAME,
"--import-text", pemText,
"--import-password", new String(IMPORT_PASSWORD)
"--pem-text", pemText,
"--pem-password", new String(IMPORT_PASSWORD)
);
assertEquals("",tester.getError());
assertEquals(ExitCodes.SUCCESS,tester.getResult());
Expand Down
24 changes: 24 additions & 0 deletions convex-core/src/main/java/convex/core/crypto/BIP39.java
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@ public class BIP39 {
}
}

/**
* Gets a BIP39 seed given a mnemonic and passphrase
* @param words Mnemonic words
* @param passphrase Optional BIP39 passphrase
* @return Blob containing BIP39 seed (64 bytes)
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
public static Blob getSeed(List<String> words, String passphrase) throws NoSuchAlgorithmException, InvalidKeySpecException {
if (passphrase==null) passphrase="";

Expand All @@ -228,6 +236,22 @@ public static Blob getSeed(List<String> words, String passphrase) throws NoSuchA
return getSeedInternal(pass,passphrase);
}

public static AKeyPair seedToKeyPair(Blob seed) {
long n=seed.count();
if (n!=SEED_LENGTH) {
throw new IllegalArgumentException("Expected "+SEED_LENGTH+ " byte seed but was: "+n);
}
return AKeyPair.create(seed.getContentHash().toFlatBlob());
}

/**
* Gets a BIP39 seed given a mnemonic and passphrase
* @param mnemonic Mnemonic words
* @param passphrase Optional BIP39 passphrase
* @return Blob containing BIP39 seed (64 bytes)
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
public static Blob getSeed(String mnemonic, String passphrase) throws NoSuchAlgorithmException, InvalidKeySpecException {
mnemonic=normaliseSpaces(mnemonic);
mnemonic=Normalizer.normalize(mnemonic, Normalizer.Form.NFKD);
Expand Down
2 changes: 1 addition & 1 deletion convex-core/src/main/java/convex/core/data/Address.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static Address create(long number) {
* @return Address instance, or null if not valid
*/
public static Address create(ABlob b) {
if (b.count()!=BYTE_LENGTH) return null;
if (b.count()>BYTE_LENGTH) return null;
return create(b.longValue());
}

Expand Down
2 changes: 1 addition & 1 deletion convex-core/src/main/java/convex/core/lang/Core.java
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@ public Context invoke(Context context, ACell[] args) {
Address address = RT.castAddress(o);
if (address == null) {
if (o instanceof AString) return context.withArgumentError("String not convertible to a valid Address: " + o);
if (o instanceof ABlob) return context.withArgumentError("Blob not convertiable a valid Address: " + o);
if (o instanceof ABlob) return context.withArgumentError("Blob not convertible a valid Address: " + o);
return context.withCastError(0,args, Types.ADDRESS);
}
long juice = Juice.ADDRESS;
Expand Down
2 changes: 2 additions & 0 deletions convex-core/src/main/java/convex/core/util/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,8 @@ public static long getCurrentTimestamp() {
private static final long startupNanos=System.nanoTime();

public static final Object[] EMPTY_OBJECTS = new Object[0];

public static final char[] EMPTY_CHARS = new char[0];

/**
* Gets a millisecond accurate time suitable for use in timing.
Expand Down
13 changes: 11 additions & 2 deletions convex-core/src/test/java/convex/core/crypto/BIP39Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.List;

import org.junit.jupiter.api.Test;

import convex.core.data.Blob;

public class BIP39Test {

@Test public void testWordList() {
Expand All @@ -19,7 +22,11 @@ public class BIP39Test {
public void testSeed() throws NoSuchAlgorithmException, InvalidKeySpecException {
List<String> tw1=List.of("blue claw trip feature street glue element derive dentist rose daring cash".split(" "));
String exSeed="8212cc694344bbc4ae70505948c58194c16cd10599b2e93f0f7f638aaa108009a5707f9274fc6bdeb23bf30783d0c2c7bb556a7aa7b9064dab6df9b8c469e39c";
assertEquals(exSeed,BIP39.getSeed(tw1,"").toHexString());
Blob seed = BIP39.getSeed(tw1,"");
assertEquals(exSeed,seed.toHexString());

AKeyPair kp=BIP39.seedToKeyPair(seed);
assertNotNull(kp);
}

/**
Expand All @@ -32,7 +39,9 @@ public void testExample15() throws NoSuchAlgorithmException, InvalidKeySpecExcep
String s1="slush blind shaft return gentle isolate notice busy silent toast joy again almost perfect century";
List<String> tw1=List.of(s1.split(" "));
String exSeed="8ac1d802490b34488eb265d72b3de8aa4cbe4ad0c674ccc083463a3cb9466ab11933f6251aec5b6b2260442435bd2f5257aa3fc219745f642295d8b6e401fe3f";
assertEquals(exSeed,BIP39.getSeed(tw1,"").toHexString());
Blob seed = BIP39.getSeed(tw1,"");
assertEquals(exSeed,seed.toHexString());
assertEquals(BIP39.SEED_LENGTH,seed.count());

String s2=s1.replaceAll(" " , " ");
assertNotEquals(s1,s2);
Expand Down
10 changes: 8 additions & 2 deletions convex-core/src/test/java/convex/core/lang/CoreTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,16 @@ public void testAddress() {
// bad arities
assertArityError(step("(address 1 2)"));
assertArityError(step("(address)"));

// Short blob / string addresses
Address ash=Address.fromHex("1234abcd");
assertEquals(305441741L,ash.longValue());
assertEquals(ash, eval("(address \"1234abcd\")"));
assertEquals(ash, eval("(address 0x1234abcd)"));

// invalid address lengths - not a cast error since argument types (in general) are valid
assertArgumentError(step("(address \"1234abcd\")"));
assertArgumentError(step("(address 0x1234abcd)"));
//assertArgumentError(step("(address \"1234abcd\")"));
//assertArgumentError(step("(address 0x1234abcd)"));

// invalid conversions
assertCastError(step("(address :foo)"));
Expand Down
Loading

0 comments on commit 50b685b

Please sign in to comment.