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

Minor modifications to PKCS5KeyFile #265

Merged
merged 7 commits into from
Aug 19, 2016
Merged
Show file tree
Hide file tree
Changes from all 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
160 changes: 100 additions & 60 deletions src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS5KeyFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,33 @@ public String getName() {
}
}

/**
* Indicates a format issue with PKCS5 data
*/
public static class FormatException
extends IOException {

FormatException(String msg) {
super(msg);
}
}

/**
* Indicates a problem decrypting the data
*/
public static class DecryptException
extends IOException {

DecryptException(String msg) {
super(msg);
}
}

protected PasswordFinder pwdf;
protected Resource<?> resource;
protected KeyPair kp;

protected KeyType type;

protected char[] passphrase; // for blanking out
protected byte[] data;

@Override
public PrivateKey getPrivate()
Expand Down Expand Up @@ -115,9 +135,9 @@ public void init(String privateKey, String publicKey, PasswordFinder pwdf) {

protected KeyPair readKeyPair()
throws IOException {

BufferedReader reader = new BufferedReader(resource.getReader());
try {
String type = null;
String line = null;
Cipher cipher = new NoneCipher();
StringBuffer sb = new StringBuffer();
Expand All @@ -126,85 +146,56 @@ protected KeyPair readKeyPair()
if (line.startsWith("-----BEGIN ") && line.endsWith(" PRIVATE KEY-----")) {
int end = line.length() - 17;
if (end > 11) {
type = line.substring(11, line.length() - 17);
String s = line.substring(11, line.length() - 17);
if ("RSA".equals(s)) {
type = KeyType.RSA;
} else if ("DSA".equals(s)) {
type = KeyType.DSA;
} else if ("DSS".equals(s)) {
type = KeyType.DSA;
} else {
throw new FormatException("Unrecognized PKCS5 key type: " + s);
}
} else {
type = "UNKNOWN";
throw new FormatException("Bad header; possibly PKCS8 format?");
}
} else if (line.startsWith("-----END")) {
break;
} else if (type != null) {
if (line.startsWith("Proc-Type: ")) {
if (!"4,ENCRYPTED".equals(line.substring(11))) {
throw new IOException("Unrecognized Proc-Type: " + line.substring(11));
throw new FormatException("Unrecognized Proc-Type: " + line.substring(11));
}
} else if (line.startsWith("DEK-Info: ")) {
int ptr = line.indexOf(",");
if (ptr == -1) {
throw new IOException("Unrecognized DEK-Info: " + line.substring(10));
throw new FormatException("Unrecognized DEK-Info: " + line.substring(10));
} else {
String algorithm = line.substring(10,ptr);
Copy link
Owner

Choose a reason for hiding this comment

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

That one seems like a false positive, i.e. this is a local variable not a field.

if ("DES-EDE3-CBC".equals(algorithm)) {
cipher = new TripleDESCBC();
iv = DatatypeConverter.parseHexBinary(line.substring(ptr+1));
} else if ("AES-128-CBC".equals(algorithm)) {
cipher = new AES128CBC();
iv = DatatypeConverter.parseHexBinary(line.substring(ptr+1));
} else if ("AES-192-CBC".equals(algorithm)) {
cipher = new AES192CBC();
iv = Base64.decode(line.substring(ptr+1));
} else if ("AES-256-CBC".equals(algorithm)) {
cipher = new AES256CBC();
iv = Base64.decode(line.substring(ptr+1));
} else {
throw new IOException("Not a supported algorithm: " + algorithm);
throw new FormatException("Not a supported algorithm: " + algorithm);
}
iv = Arrays.copyOfRange(DatatypeConverter.parseHexBinary(line.substring(ptr+1)), 0, Math.min(cipher.getIVSize(), 8));
}
} else if (line.length() > 0) {
sb.append(line);
}
}
}

byte[] data = Base64.decode(sb.toString());
if (pwdf != null) {
boolean decrypted = false;
do {
CharBuffer cb = CharBuffer.wrap(pwdf.reqPassword(resource));
ByteBuffer bb = IOUtils.UTF8.encode(cb);
byte[] passphrase = Arrays.copyOfRange(bb.array(), bb.position(), bb.limit());
Arrays.fill(cb.array(), '\u0000');
Arrays.fill(bb.array(), (byte)0);
byte[] key = new byte[cipher.getBlockSize()];
iv = Arrays.copyOfRange(iv, 0, cipher.getIVSize());
Digest md5 = new MD5();
md5.init();
int hsize = md5.getBlockSize();
byte[] hn = new byte[key.length / hsize * hsize + (key.length % hsize == 0 ? 0 : hsize)];
byte[] tmp = null;
for (int i=0; i + hsize <= hn.length;) {
if (tmp != null) {
md5.update(tmp, 0, tmp.length);
}
md5.update(passphrase, 0, passphrase.length);
md5.update(iv, 0, iv.length > 8 ? 8 : iv.length);
tmp = md5.digest();
System.arraycopy(tmp, 0, hn, i, tmp.length);
i += tmp.length;
}
Arrays.fill(passphrase, (byte)0);
System.arraycopy(hn, 0, key, 0, key.length);
cipher.init(Cipher.Mode.Decrypt, key, iv);
Arrays.fill(key, (byte)0);
cipher.update(data, 0, data.length);
decrypted = 0x30 == data[0];
} while (!decrypted && pwdf.shouldRetry(resource));
if (type == null) {
throw new FormatException("PKCS5 header not found");
}
if (0x30 != data[0]) {
throw new IOException("Failed to decrypt key");
}

ASN1Data asn = new ASN1Data(data);
if ("RSA".equals(type)) {
ASN1Data asn = new ASN1Data(data = decrypt(Base64.decode(sb.toString()), cipher, iv));
switch(type) {
case RSA: {
KeyFactory factory = KeyFactory.getInstance("RSA");
asn.readNext();
BigInteger modulus = asn.readNext();
Expand All @@ -213,7 +204,8 @@ protected KeyPair readKeyPair()
PublicKey pubKey = factory.generatePublic(new RSAPublicKeySpec(modulus, pubExp));
PrivateKey prvKey = factory.generatePrivate(new RSAPrivateKeySpec(modulus, prvExp));
return new KeyPair(pubKey, prvKey);
} else if ("DSA".equals(type)) {
}
case DSA: {
KeyFactory factory = KeyFactory.getInstance("DSA");
asn.readNext();
BigInteger p = asn.readNext();
Expand All @@ -224,8 +216,9 @@ protected KeyPair readKeyPair()
PublicKey pubKey = factory.generatePublic(new DSAPublicKeySpec(pub, p, q, g));
PrivateKey prvKey = factory.generatePrivate(new DSAPrivateKeySpec(prv, p, q, g));
return new KeyPair(pubKey, prvKey);
} else {
throw new IOException("Unrecognized key type: " + type);
}
default:
throw new IOException("Unrecognized PKCS5 key type: " + type);
}
} catch (NoSuchAlgorithmException e) {
throw new IOException(e);
Expand All @@ -241,15 +234,62 @@ public String toString() {
return "PKCS5KeyFile{resource=" + resource + "}";
}

private byte[] getPassphraseBytes() {
CharBuffer cb = CharBuffer.wrap(pwdf.reqPassword(resource));
ByteBuffer bb = IOUtils.UTF8.encode(cb);
byte[] result = Arrays.copyOfRange(bb.array(), bb.position(), bb.limit());
Arrays.fill(cb.array(), '\u0000');
Arrays.fill(bb.array(), (byte)0);
return result;
}

private byte[] decrypt(byte[] raw, Cipher cipher, byte[] iv) throws DecryptException {
if (pwdf == null) {
return raw;
}
Digest md5 = new MD5();
int bsize = cipher.getBlockSize();
int hsize = md5.getBlockSize();
int hnlen = bsize / hsize * hsize + (bsize % hsize == 0 ? 0 : hsize);
do {
md5.init();
byte[] hn = new byte[hnlen];
byte[] tmp = null;
byte[] passphrase = getPassphraseBytes();
for (int i=0; i + hsize <= hn.length;) {
if (tmp != null) {
md5.update(tmp, 0, tmp.length);
}
md5.update(passphrase, 0, passphrase.length);
md5.update(iv, 0, iv.length);
tmp = md5.digest();
System.arraycopy(tmp, 0, hn, i, tmp.length);
i += tmp.length;
}
Arrays.fill(passphrase, (byte)0);
byte[] key = Arrays.copyOfRange(hn, 0, bsize);
cipher.init(Cipher.Mode.Decrypt, key, iv);
Arrays.fill(key, (byte)0);
byte[] decrypted = Arrays.copyOf(raw, raw.length);
cipher.update(decrypted, 0, decrypted.length);
if (ASN1Data.MAGIC == decrypted[0]) {
return decrypted;
}
} while (pwdf.shouldRetry(resource));
throw new DecryptException("Decryption failed");
}

class ASN1Data {
static final byte MAGIC = (byte)0x30;

private byte[] buff;
private int index, length;

ASN1Data(byte[] buff) throws IOException {
ASN1Data(byte[] buff) throws FormatException {
this.buff = buff;
index = 0;
if (buff[index++] != (byte)0x30) {
throw new IOException("Not ASN.1 data");
if (buff[index++] != MAGIC) {
throw new FormatException("Not ASN.1 data");
}
length = buff[index++] & 0xff;
if ((length & 0x80) != 0) {
Expand All @@ -260,7 +300,7 @@ class ASN1Data {
}
}
if ((index + length) > buff.length) {
throw new IOException("Length mismatch: " + buff.length + " != " + (index + length));
throw new FormatException("Length mismatch: " + buff.length + " != " + (index + length));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import net.schmizz.sshj.common.KeyType;
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
import net.schmizz.sshj.userauth.keyprovider.PKCS5KeyFile;
import net.schmizz.sshj.userauth.password.PasswordFinder;
import net.schmizz.sshj.userauth.password.Resource;
import net.schmizz.sshj.util.KeyUtil;
import org.junit.Before;
import org.junit.Test;
Expand All @@ -37,6 +39,15 @@ public class PKCS5KeyFileTest {
static final String pubExp = "23";
static final String privExp = "57bcee2e2656eb2c94033d802dd62d726c6705fabad1fd0df86b67600a96431301620d395cbf5871c7af3d3974dfe5c30f5c60d95d7e6e75df69ed6c5a36a9c8aef554b5058b76a719b8478efa08ad1ebf08c8c25fe4b9bc0bfbb9be5d4f60e6213b4ab1c26ad33f5bba7d93e1cd65f65f5a79eb6ebfb32f930a2b0244378b4727acf83b5fb376c38d4abecc5dc3fc399e618e792d4c745d2dbbb9735242e5033129f2985ca3e28fa33cad2afe3e70e1b07ed2b6ec8a3f843fb4bffe3385ad211c6600618488f4ac70397e8eb036b82d811283dc728504cddbe1533c4dd31b1ec99ffa74fd0e3883a9cb3e2315cc1a56f55d38ed40520dd9ec91b4d2dd790d1b";

static final String g = "23b0484f5ad9cba2b3dba7129419fbec7f8c014e22d3b19de4ebbca20d0ebd2e9f5225dabdd48de75f87e3193377fb1072c08433f82f6e6e581a319d4fc7d283cdcd2ae2000fe572c0a800fd47b7590d6a6afe3df54aedd57696c6538029daebf11d9e277edc0c7e905e237d3b9e6a6f674d83da5cc0131ac0be2e55ac69730e";
static final String p = "92b746cf7c0e9ea35fd9b09b0c3dbdfde453468984698ff168fefef3f0457d29bcf81c88830ac1099223d00745423e44cdef66f4cdc3fad1d95ce2868b3e885c1d518c9fcda597d5c373f05f6f323553f60bd992404183dab41d82ab6d3b3ecf2dfc3c136fa67c4312ec0b7bbac77a634e1eb5dd9a62efd0ddab477d0b49c0b9";
static final String q = "96a05e07b9e52d6f1137d11d5d270b568b94162f";
static final String x = "8981aebb71c60b5951f0ab3ed1a00b5307742f43";
static final String y = "7e845aada202d31004c52ab170cbe62ce9a962b9f4acbc67a57f62eb090a67b3faa53d38050f87b2b66ddf1185472f27842c3e3e58d025f9148a28f49ebdfb6efefee8ee10fe84a2d56535dddb301dfee15538108639e8a0ec7aa237ddb999f35b6a5c6b875052998233374163ad031f974d29c2631394436ae186b418348193";
final char[] correctPassphrase = "test_passphrase".toCharArray();
final char[] incorrectPassphrase = new char[]{' '};


@Before
public void setUp()
throws UnsupportedEncodingException, GeneralSecurityException {
Expand All @@ -56,4 +67,30 @@ public void testType()
assertEquals(rsa.getType(), KeyType.RSA);
}

final PasswordFinder givesOn3rdTry = new PasswordFinder() {
int triesLeft = 3;

@Override
public char[] reqPassword(Resource resource) {
if (triesLeft == 0)
return correctPassphrase;
else {
triesLeft--;
return incorrectPassphrase;
}
}

@Override
public boolean shouldRetry(Resource resource) {
return triesLeft >= 0;
}
};

@Test
public void retries()
throws IOException, GeneralSecurityException {
FileKeyProvider dsa = new PKCS5KeyFile();
dsa.init(new File("src/test/resources/id_dsa"), givesOn3rdTry);
assertEquals(KeyUtil.newDSAPrivateKey(x, p, q, g), dsa.getPrivate());
}
}