Skip to content

Commit

Permalink
Support OpenSSH encrypted ED25519 keys
Browse files Browse the repository at this point in the history
  • Loading branch information
mc1arke committed Apr 10, 2017
1 parent 00b24a1 commit dac7b1b
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 11 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Expand Up @@ -54,6 +54,11 @@
<artifactId>eddsa</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.connectbot.jbcrypt</groupId>
<artifactId>jbcrypt</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>

<build>
Expand Down
117 changes: 108 additions & 9 deletions src/com/trilead/ssh2/signature/ED25519KeyAlgorithm.java
Expand Up @@ -2,6 +2,10 @@

import com.trilead.ssh2.crypto.CertificateDecoder;
import com.trilead.ssh2.crypto.PEMStructure;
import com.trilead.ssh2.crypto.cipher.BlockCipher;
import com.trilead.ssh2.crypto.cipher.BlockCipherFactory;
import com.trilead.ssh2.crypto.cipher.CBCMode;
import com.trilead.ssh2.crypto.cipher.DES;
import com.trilead.ssh2.packets.TypesReader;
import com.trilead.ssh2.packets.TypesWriter;
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
Expand All @@ -11,6 +15,7 @@
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
import org.mindrot.jbcrypt.BCrypt;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -121,7 +126,7 @@ public KeyPair createKeyPair(PEMStructure pemStructure, String password) throws

String cipher = pemReader.readString();
String kdf = pemReader.readString();
/*byte[] kdfOptions = */pemReader.readByteString(); //not used until we get key decryption sorted
byte[] kdfOptions = pemReader.readByteString();
int keyCount = pemReader.readUINT32();

// I can't actually find any test cases for multiple keys to know how they're used
Expand All @@ -137,14 +142,11 @@ public KeyPair createKeyPair(PEMStructure pemStructure, String password) throws
throw new IOException("PEM is encrypted but password has not been specified");
}

//TODO handle encrypted keys
/*
https://github.com/openssh/openssh-portable/blob/master/openbsd-compat/bcrypt_pbkdf.c
indicates open-ssh deviates from the normal implementation of bcrypt, so I'll need to check
whether I can find a Java library that supports this, or whether I have to modify an
existing implementation... or even write my own.
*/
throw new IOException("Encrypted OpenSSH keys are not currently supported");
TypesReader kdfReader = new TypesReader(kdfOptions);
byte[] salt = kdfReader.readByteString();
int rounds = kdfReader.readUINT32();
SshCipher sshCipher = SshCipher.getInstance(cipher);
privateKeys = decryptData(privateKeys, generateKayAndIvPbkdf2(password.getBytes(StandardCharsets.UTF_8), salt, rounds, sshCipher.getKeyLength(), sshCipher.getBlockSize()), sshCipher);
} else if (!"none".equals(cipher) || !"none".equals(kdf)) {
throw new IOException("Unexpected encryption method for key");
}
Expand Down Expand Up @@ -188,6 +190,103 @@ public KeyPair createKeyPair(PEMStructure pemStructure, String password) throws
throw new IOException("Could not create EcDSA key pair", ex);
}
}

private static byte[] decryptData(byte[] encryptedData, byte[] keyAndIv, SshCipher sshCipher) {
byte[] key = new byte[sshCipher.getKeyLength()];
byte[] iv = new byte[sshCipher.getBlockSize()];

System.arraycopy(keyAndIv, 0, key, 0, key.length);
System.arraycopy(keyAndIv, key.length, iv, 0, iv.length);

BlockCipher cipher = sshCipher.createBlockCipher(key, iv, false);

byte[] decrypted = new byte[encryptedData.length];
for (int i = 0; i < encryptedData.length / cipher.getBlockSize(); i++) {
cipher.transformBlock(encryptedData, i * cipher.getBlockSize(), decrypted, i * cipher.getBlockSize());
}

return decrypted;

}

private static byte[] generateKayAndIvPbkdf2(byte[] password, byte[] salt, int rounds, int keyLength, int ivLength) {
byte[] keyAndIV = new byte[keyLength + ivLength];
new BCrypt().pbkdf(password, salt, rounds, keyAndIV);
return keyAndIV;
}

private enum SshCipher {

DESEDE_CBC(24, 8, "des-ede3-cbc") {
@Override
BlockCipher createBlockCipher(byte[] key, byte[] iv, boolean encrypt) {
return BlockCipherFactory.createCipher("3des-cbc", encrypt, key, iv);
}
},
DES_CBC(8, 8,"des-cbc") {
@Override
BlockCipher createBlockCipher(byte[]key, byte[] iv, boolean encrypt) {
DES des = new DES();
des.init(encrypt, key);
return new CBCMode(des, iv, encrypt);
}
},
AES128_CBC(16, 16, "aes-128-cbc", "aes128-cbc") {
@Override
BlockCipher createBlockCipher(byte[] key, byte[] iv, boolean encrypt) {
return BlockCipherFactory.createCipher("aes128-cbc", encrypt, key, iv);
}
},
AES192_CBC(24, 16, "aes-192-cbc", "aes192-cbc") {
@Override
BlockCipher createBlockCipher(byte[] key, byte[] iv, boolean encrypt) {
return BlockCipherFactory.createCipher("aes192-cbc", encrypt, key, iv);
}
},
AES256_CBC(32, 16, "aes-256-cbc", "aes256-cbc") {
@Override
BlockCipher createBlockCipher(byte[] key, byte[] iv, boolean encrypt) {
return BlockCipherFactory.createCipher("aes256-cbc", encrypt, key, iv);
}
};

private final String[] sshCipherNames;
private final int keyLength;
private final int blockSize;

SshCipher(int keyLength, int blockSize, String cipherName, String... cipherAliases) {
this.keyLength = keyLength;
this.blockSize = blockSize;
String[] sshCipherNames = new String[1 + (null == cipherAliases ? 0 : cipherAliases.length)];
sshCipherNames[0] = cipherName;
if (null != cipherAliases) {
System.arraycopy(cipherAliases, 0, sshCipherNames, 1, cipherAliases.length);
}
this.sshCipherNames = sshCipherNames;
}

abstract BlockCipher createBlockCipher(byte[] key, byte[] iv, boolean encrypt);

public int getBlockSize() {
return blockSize;
}

public int getKeyLength() {
return keyLength;
}

public static SshCipher getInstance(String cipher) {
for (SshCipher instance : values()) {
for (String name : instance.sshCipherNames) {
if (name.equalsIgnoreCase(cipher)) {
return instance;
}
}
}
throw new IllegalArgumentException("Unknown Cipher: " + cipher);
}

}


}
Expand Down
7 changes: 5 additions & 2 deletions test/com/trilead/ssh2/signature/ED25519KeyAlgorithmTest.java
Expand Up @@ -73,7 +73,10 @@ public void testSignAndVerifyFailure() throws GeneralSecurityException, IOExcept

@Test
public void testParsePrivateKey() throws IOException {
PEMDecoder.decodeKeyPair(IOUtils.toCharArray(getClass().getResourceAsStream("ed25519-testkey-unprotected.txt")), null);
//Once we get decryption working: PEMDecoder.decodeKeyPair(IOUtils.toCharArray(getClass().getResourceAsStream("ed25519-testkey-protected.txt")), "password");
KeyPair expected = PEMDecoder.decodeKeyPair(IOUtils.toCharArray(getClass().getResourceAsStream("ed25519-testkey-unprotected.txt")), null);
KeyPair actual = PEMDecoder.decodeKeyPair(IOUtils.toCharArray(getClass().getResourceAsStream("ed25519-testkey-protected.txt")), "password");

assertEquals(expected.getPrivate(), actual.getPrivate());
assertEquals(expected.getPublic(), actual.getPublic());
}
}

0 comments on commit dac7b1b

Please sign in to comment.