Skip to content

Commit

Permalink
Add possibility to export keys in Crypt4GH format
Browse files Browse the repository at this point in the history
  • Loading branch information
dtitov committed Nov 28, 2019
1 parent b03e070 commit 2f8aa08
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 25 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ To include this library to your Maven project add following to the `pom.xml`:
To install console app you can use the following script (assuming you are using `bash`):
```
PREFIX=/usr/local/bin && \
sudo curl -L "https://github.com/uio-bmi/crypt4gh/releases/download/v2.1.0/crypt4gh.jar" -o "$PREFIX/crypt4gh.jar" && \
sudo curl -L "https://github.com/uio-bmi/crypt4gh/releases/download/v2.2.0/crypt4gh.jar" -o "$PREFIX/crypt4gh.jar" && \
echo -e '#!/usr/bin/env bash\njava -jar' "$PREFIX/crypt4gh.jar" '$@' | sudo tee "$PREFIX/crypt4gh" > /dev/null && \
sudo chmod +x "$PREFIX/crypt4gh"
```
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>no.uio.ifi</groupId>
<artifactId>crypt4gh</artifactId>
<version>2.1.0</version>
<version>2.2.0</version>
<packaging>jar</packaging>

<name>crypt4gh</name>
Expand Down
16 changes: 13 additions & 3 deletions src/main/java/no/uio/ifi/crypt4gh/app/Crypt4GHUtils.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package no.uio.ifi.crypt4gh.app;

import no.uio.ifi.crypt4gh.pojo.key.Format;
import no.uio.ifi.crypt4gh.stream.Crypt4GHInputStream;
import no.uio.ifi.crypt4gh.stream.Crypt4GHOutputStream;
import no.uio.ifi.crypt4gh.util.KeyUtils;
Expand Down Expand Up @@ -32,18 +33,27 @@ static Crypt4GHUtils getInstance() {
private Crypt4GHUtils() {
}

void generateX25519KeyPair(String keyName) throws Exception {
void generateX25519KeyPair(String keyName, String keyFormat) throws Exception {
KeyUtils keyUtils = KeyUtils.getInstance();
KeyPair keyPair = keyUtils.generateKeyPair();
File pubFile = new File(keyName + ".pub.pem");
if (!pubFile.exists() || pubFile.exists() &&
consoleUtils.promptForConfirmation("Public key file already exists: do you want to overwrite it?")) {
keyUtils.writePEMFile(pubFile, keyPair.getPublic());
if (Format.CRYPT4GH.name().equalsIgnoreCase(keyFormat)) {
keyUtils.writeCrypt4GHKey(pubFile, keyPair.getPublic(), null);
} else {
keyUtils.writeOpenSSLKey(pubFile, keyPair.getPublic());
}
}
File secFile = new File(keyName + ".sec.pem");
if (!secFile.exists() || secFile.exists() &&
consoleUtils.promptForConfirmation("Private key file already exists: do you want to overwrite it?")) {
keyUtils.writePEMFile(secFile, keyPair.getPrivate());
if (Format.CRYPT4GH.name().equalsIgnoreCase(keyFormat)) {
char[] password = consoleUtils.readPassword("Password for the private key: ", 4);
keyUtils.writeCrypt4GHKey(secFile, keyPair.getPrivate(), password);
} else {
keyUtils.writeOpenSSLKey(secFile, keyPair.getPrivate());
}
}
Set<PosixFilePermission> perms = new HashSet<>();
perms.add(PosixFilePermission.OWNER_READ);
Expand Down
7 changes: 5 additions & 2 deletions src/main/java/no/uio/ifi/crypt4gh/app/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class Main {
public static final String GENERATE = "g";
public static final String ENCRYPT = "e";
public static final String DECRYPT = "d";
public static final String KEY_FORMAT = "kf";
public static final String PUBLIC_KEY = "pk";
public static final String SECRET_KEY = "sk";
public static final String VERSION = "v";
Expand All @@ -25,13 +26,15 @@ public static void main(String[] args) throws Exception {
Options options = new Options();

OptionGroup mainOptions = new OptionGroup();
mainOptions.addOption(new Option(GENERATE, "generate", true, "generate key pair (specify desired key name)"));
Option generateKeyOption = new Option(GENERATE, "generate", true, "generate key pair (specify desired key name)");
mainOptions.addOption(generateKeyOption);
mainOptions.addOption(new Option(ENCRYPT, "encrypt", true, "encrypt the file (specify file to encrypt)"));
mainOptions.addOption(new Option(DECRYPT, "decrypt", true, "decrypt the file (specify file to decrypt)"));
mainOptions.addOption(new Option(VERSION, "version", false, "print application's version"));
mainOptions.addOption(new Option(HELP, "help", false, "print this message"));
options.addOptionGroup(mainOptions);

options.addOption(new Option(KEY_FORMAT, "keyformat", true, "key format to use for generated keys (OpenSSL or Crypt4GH)"));
options.addOption(new Option(PUBLIC_KEY, "pubkey", true, "public key to use (specify key file)"));
options.addOption(new Option(SECRET_KEY, "seckey", true, "secret key to use (specify key file)"));

Expand All @@ -48,7 +51,7 @@ public static void main(String[] args) throws Exception {
} else if (line.hasOption(VERSION)) {
printVersion();
} else if (line.hasOption(GENERATE)) {
crypt4GHUtils.generateX25519KeyPair(line.getOptionValue(GENERATE));
crypt4GHUtils.generateX25519KeyPair(line.getOptionValue(GENERATE), line.getOptionValue(KEY_FORMAT));
} else {
if (line.hasOption(ENCRYPT)) {
if (!line.hasOption(PUBLIC_KEY)) {
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/no/uio/ifi/crypt4gh/pojo/key/Format.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package no.uio.ifi.crypt4gh.pojo.key;

/**
* Key format name
*/
public enum Format {

OPENSSL, CRYPT4GH

}
93 changes: 75 additions & 18 deletions src/main/java/no/uio/ifi/crypt4gh/util/KeyUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
Expand All @@ -27,6 +28,7 @@
import java.util.Base64;
import java.util.Collection;

import static at.favre.lib.crypto.bcrypt.BCrypt.SALT_LENGTH;
import static no.uio.ifi.crypt4gh.pojo.header.X25519ChaCha20IETFPoly1305HeaderPacket.CHA_CHA_20_POLY_1305;
import static no.uio.ifi.crypt4gh.pojo.header.X25519ChaCha20IETFPoly1305HeaderPacket.NONCE_SIZE;

Expand All @@ -39,9 +41,13 @@ public class KeyUtils {
public static final String X25519 = "X25519";

public static final String BEGIN_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----";
public static final String BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----";
public static final String END_PUBLIC_KEY = "-----END PUBLIC KEY-----";
public static final String BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----";
public static final String END_PRIVATE_KEY = "-----END PRIVATE KEY-----";
public static final String BEGIN_CRYPT4GH_PUBLIC_KEY = "-----BEGIN CRYPT4GH PUBLIC KEY-----";
public static final String END_CRYPT4GH_PUBLIC_KEY = "-----END CRYPT4GH PUBLIC KEY-----";
public static final String BEGIN_CRYPT4GH_ENCRYPTED_PRIVATE_KEY = "-----BEGIN CRYPT4GH ENCRYPTED PRIVATE KEY-----";
public static final String END_CRYPT4GH_ENCRYPTED_PRIVATE_KEY = "-----END CRYPT4GH ENCRYPTED PRIVATE KEY-----";

public static final String CRYPT4GH_AUTH_MAGIC = "c4gh-v1";

Expand Down Expand Up @@ -293,7 +299,7 @@ public PrivateKey readPrivateKey(String keyMaterial, char[] password) throws Gen
public PrivateKey readCrypt4GHPrivateKey(byte[] keyMaterial, char[] password) throws GeneralSecurityException, IllegalArgumentException {
ByteBuffer byteBuffer = ByteBuffer.wrap(keyMaterial).order(ByteOrder.BIG_ENDIAN);
byteBuffer.get(new byte[CRYPT4GH_AUTH_MAGIC.length()]);
KDF kdf = KDF.valueOf(readString(byteBuffer).toUpperCase());
KDF kdf = KDF.valueOf(decodeString(byteBuffer).toUpperCase());
int rounds = 0;
byte[] salt = new byte[0];
if (kdf != KDF.NONE) {
Expand All @@ -303,11 +309,11 @@ public PrivateKey readCrypt4GHPrivateKey(byte[] keyMaterial, char[] password) th
short roundsAndSaltLength = byteBuffer.getShort();
int saltLength = roundsAndSaltLength - 4;
rounds = byteBuffer.getInt();
salt = readArray(byteBuffer, saltLength);
salt = decodeArray(byteBuffer, saltLength);
}
Cipher cipher = Cipher.valueOf(readString(byteBuffer).toUpperCase());
Cipher cipher = Cipher.valueOf(decodeString(byteBuffer).toUpperCase());
short keyLength = byteBuffer.getShort();
byte[] payload = readArray(byteBuffer, keyLength);
byte[] payload = decodeArray(byteBuffer, keyLength);
if (kdf == KDF.NONE) {
if (cipher != Cipher.NONE) {
throw new GeneralSecurityException("Invalid private key: KDF is 'none', but cipher is not 'none");
Expand All @@ -324,17 +330,6 @@ public PrivateKey readCrypt4GHPrivateKey(byte[] keyMaterial, char[] password) th
return constructPrivateKey(decryptedPayload);
}

private String readString(ByteBuffer byteBuffer) {
short length = byteBuffer.getShort();
return new String(readArray(byteBuffer, length));
}

private byte[] readArray(ByteBuffer byteBuffer, int length) {
byte[] array = new byte[length];
byteBuffer.get(array);
return array;
}

/**
* Decodes Base64 key string, surrounded by header and footer.
*
Expand All @@ -347,13 +342,13 @@ public byte[] decodeKey(String keyMaterial) {
}

/**
* Writes the key to a file.
* Writes the key to a file in OpenSSL format.
*
* @param keyFile Key file to create.
* @param key Key to write.
* @throws IOException If the file can't be written.
*/
public void writePEMFile(File keyFile, Key key) throws IOException {
public void writeOpenSSLKey(File keyFile, Key key) throws IOException {
Collection<String> keyLines = new ArrayList<>();
boolean isPublic = key instanceof PublicKey;
if (isPublic) {
Expand All @@ -370,6 +365,68 @@ public void writePEMFile(File keyFile, Key key) throws IOException {
FileUtils.writeLines(keyFile, keyLines);
}

/**
* Writes the key to a file in Crypt4GH format.
*
* @param keyFile Key file to create.
* @param key Key to write.
* @param password Password to lock private key.
* @throws IOException If the file can't be written.
*/
public void writeCrypt4GHKey(File keyFile, Key key, char[] password) throws IOException, GeneralSecurityException {
Collection<String> keyLines = new ArrayList<>();
boolean isPublic = key instanceof PublicKey;
byte[] encodedKey = encodeKey(key);
if (isPublic) {
keyLines.add(BEGIN_CRYPT4GH_PUBLIC_KEY);
keyLines.add(Base64.getEncoder().encodeToString(encodedKey));
keyLines.add(END_CRYPT4GH_PUBLIC_KEY);
} else {
byte[] salt = new byte[SALT_LENGTH];
SecureRandom.getInstanceStrong().nextBytes(salt);
SecretKeySpec derivedKey = new SecretKeySpec(KDF.SCRYPT.derive(0, password, salt), CHA_CHA_20);
Arrays.fill(password, (char) 0);
byte[] nonce = new byte[NONCE_SIZE];
SecureRandom.getInstanceStrong().nextBytes(nonce);
javax.crypto.Cipher encryption = javax.crypto.Cipher.getInstance(CHA_CHA_20_POLY_1305);
encryption.init(javax.crypto.Cipher.ENCRYPT_MODE, derivedKey, new IvParameterSpec(nonce));
byte[] encryptedKey = encryption.doFinal(encodedKey);

keyLines.add(BEGIN_CRYPT4GH_ENCRYPTED_PRIVATE_KEY);
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
byteArrayOutputStream.write(CRYPT4GH_AUTH_MAGIC.getBytes());
byteArrayOutputStream.write(encodeString(KDF.SCRYPT.name().toLowerCase()));
byteArrayOutputStream.write(encodeArray(ArrayUtils.addAll(new byte[4], salt)));
byteArrayOutputStream.write(encodeString(Cipher.CHACHA20_POLY1305.name().toLowerCase()));
byteArrayOutputStream.write(encodeArray(ArrayUtils.addAll(nonce, encryptedKey)));
keyLines.add(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
}
keyLines.add(END_CRYPT4GH_ENCRYPTED_PRIVATE_KEY);
}
FileUtils.writeLines(keyFile, keyLines);
}

private String decodeString(ByteBuffer byteBuffer) {
short length = byteBuffer.getShort();
return new String(decodeArray(byteBuffer, length));
}

private byte[] decodeArray(ByteBuffer byteBuffer, int length) {
byte[] array = new byte[length];
byteBuffer.get(array);
return array;
}

private byte[] encodeString(String string) {
short length = (short) string.length();
return ByteBuffer.allocate(2 + length).order(ByteOrder.BIG_ENDIAN).putShort(length).put(string.getBytes()).array();
}

private byte[] encodeArray(byte[] array) {
short length = (short) array.length;
return ByteBuffer.allocate(2 + length).order(ByteOrder.BIG_ENDIAN).putShort(length).put(array).array();
}

private static class StaticSecureRandom extends SecureRandom {

private final byte[] privateKey;
Expand Down

0 comments on commit 2f8aa08

Please sign in to comment.