Skip to content

Commit db48ff8

Browse files
authored
Add support for encrypted ed25519 openssh-key-v1 files (Fixes #427) (#429)
1 parent 49a450f commit db48ff8

File tree

7 files changed

+1235
-3
lines changed

7 files changed

+1235
-3
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ license {
7575
mapping {
7676
java = 'SLASHSTAR_STYLE'
7777
}
78-
excludes(['**/djb/Curve25519.java', '**/sshj/common/Base64.java'])
78+
excludes(['**/djb/Curve25519.java', '**/sshj/common/Base64.java', '**/org/mindrot/jbcrypt/*.java'])
7979
}
8080

8181
// This disables the pedantic doclint feature of JDK8

src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyV1KeyFile.java

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,29 @@
1515
*/
1616
package com.hierynomus.sshj.userauth.keyprovider;
1717

18+
import com.hierynomus.sshj.transport.cipher.BlockCiphers;
1819
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
1920
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
2021
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
2122
import net.schmizz.sshj.common.*;
2223
import net.schmizz.sshj.common.Buffer.PlainBuffer;
24+
import net.schmizz.sshj.transport.cipher.Cipher;
2325
import net.schmizz.sshj.userauth.keyprovider.BaseFileKeyProvider;
2426
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
2527
import net.schmizz.sshj.userauth.keyprovider.KeyFormat;
28+
import org.mindrot.jbcrypt.BCrypt;
2629
import org.slf4j.Logger;
2730
import org.slf4j.LoggerFactory;
2831

2932
import java.io.BufferedReader;
3033
import java.io.IOException;
34+
import java.nio.ByteBuffer;
35+
import java.nio.CharBuffer;
36+
import java.nio.charset.Charset;
3137
import java.security.GeneralSecurityException;
3238
import java.security.KeyPair;
3339
import java.security.PublicKey;
40+
import java.util.Arrays;
3441

3542
/**
3643
* Reads a key file in the new OpenSSH format.
@@ -42,6 +49,7 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
4249
private static final String END = "-----END ";
4350
private static final byte[] AUTH_MAGIC = "openssh-key-v1\0".getBytes();
4451
public static final String OPENSSH_PRIVATE_KEY = "OPENSSH PRIVATE KEY-----";
52+
public static final String BCRYPT = "bcrypt";
4553

4654
public static class Factory
4755
implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
@@ -86,7 +94,7 @@ private KeyPair readDecodedKeyPair(final PlainBuffer keyBuffer) throws IOExcepti
8694

8795
String cipherName = keyBuffer.readString(); // string ciphername
8896
String kdfName = keyBuffer.readString(); // string kdfname
89-
String kdfOptions = keyBuffer.readString(); // string kdfoptions
97+
byte[] kdfOptions = keyBuffer.readBytes(); // string kdfoptions
9098

9199
int nrKeys = keyBuffer.readUInt32AsInt(); // int number of keys N; Should be 1
92100
if (nrKeys != 1) {
@@ -99,10 +107,44 @@ private KeyPair readDecodedKeyPair(final PlainBuffer keyBuffer) throws IOExcepti
99107
return readUnencrypted(privateKeyBuffer, publicKey);
100108
} else {
101109
logger.info("Keypair is encrypted with: " + cipherName + ", " + kdfName + ", " + kdfOptions);
102-
throw new IOException("Cannot read encrypted keypair with " + cipherName + " yet.");
110+
PlainBuffer decrypted = decryptBuffer(privateKeyBuffer, cipherName, kdfName, kdfOptions);
111+
return readUnencrypted(decrypted, publicKey);
112+
// throw new IOException("Cannot read encrypted keypair with " + cipherName + " yet.");
103113
}
104114
}
105115

116+
private PlainBuffer decryptBuffer(PlainBuffer privateKeyBuffer, String cipherName, String kdfName, byte[] kdfOptions) throws IOException {
117+
Cipher cipher = createCipher(cipherName);
118+
initializeCipher(kdfName, kdfOptions, cipher);
119+
byte[] array = privateKeyBuffer.array();
120+
cipher.update(array, 0, privateKeyBuffer.available());
121+
return new PlainBuffer(array);
122+
}
123+
124+
private void initializeCipher(String kdfName, byte[] kdfOptions, Cipher cipher) throws Buffer.BufferException {
125+
if (kdfName.equals(BCRYPT)) {
126+
PlainBuffer opts = new PlainBuffer(kdfOptions);
127+
CharBuffer charBuffer = CharBuffer.wrap(pwdf.reqPassword(null));
128+
ByteBuffer byteBuffer = Charset.forName("UTF-8").encode(charBuffer);
129+
byte[] passphrase = Arrays.copyOfRange(byteBuffer.array(),
130+
byteBuffer.position(), byteBuffer.limit());
131+
byte[] keyiv = new byte[48];
132+
new BCrypt().pbkdf(passphrase, opts.readBytes(), opts.readUInt32AsInt(), keyiv);
133+
byte[] key = Arrays.copyOfRange(keyiv, 0, 32);
134+
byte[] iv = Arrays.copyOfRange(keyiv, 32, 48);
135+
cipher.init(Cipher.Mode.Decrypt, key, iv);
136+
} else {
137+
throw new IllegalStateException("No support for KDF '" + kdfName + "'.");
138+
}
139+
}
140+
141+
private Cipher createCipher(String cipherName) {
142+
if (cipherName.equals(BlockCiphers.AES256CTR().getName())) {
143+
return BlockCiphers.AES256CTR().create();
144+
}
145+
throw new IllegalStateException("Cipher '" + cipherName + "' not currently implemented for openssh-key-v1 format");
146+
}
147+
106148
private PublicKey readPublicKey(final PlainBuffer plainBuffer) throws Buffer.BufferException, GeneralSecurityException {
107149
return KeyType.fromString(plainBuffer.readString()).readPubKeyFromBuffer(plainBuffer);
108150
}

0 commit comments

Comments
 (0)