Merge pull request #265 from joval/master

Minor modifications to PKCS5KeyFile
This commit is contained in:
Jeroen van Erp
2016-08-19 23:35:03 +02:00
committed by GitHub
2 changed files with 137 additions and 60 deletions

View File

@@ -50,13 +50,33 @@ public class PKCS5KeyFile
} }
} }
/**
* 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 PasswordFinder pwdf;
protected Resource<?> resource; protected Resource<?> resource;
protected KeyPair kp; protected KeyPair kp;
protected KeyType type; protected KeyType type;
protected byte[] data;
protected char[] passphrase; // for blanking out
@Override @Override
public PrivateKey getPrivate() public PrivateKey getPrivate()
@@ -115,9 +135,9 @@ public class PKCS5KeyFile
protected KeyPair readKeyPair() protected KeyPair readKeyPair()
throws IOException { throws IOException {
BufferedReader reader = new BufferedReader(resource.getReader()); BufferedReader reader = new BufferedReader(resource.getReader());
try { try {
String type = null;
String line = null; String line = null;
Cipher cipher = new NoneCipher(); Cipher cipher = new NoneCipher();
StringBuffer sb = new StringBuffer(); StringBuffer sb = new StringBuffer();
@@ -126,85 +146,56 @@ public class PKCS5KeyFile
if (line.startsWith("-----BEGIN ") && line.endsWith(" PRIVATE KEY-----")) { if (line.startsWith("-----BEGIN ") && line.endsWith(" PRIVATE KEY-----")) {
int end = line.length() - 17; int end = line.length() - 17;
if (end > 11) { 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 { } else {
type = "UNKNOWN"; throw new FormatException("Bad header; possibly PKCS8 format?");
} }
} else if (line.startsWith("-----END")) { } else if (line.startsWith("-----END")) {
break; break;
} else if (type != null) { } else if (type != null) {
if (line.startsWith("Proc-Type: ")) { if (line.startsWith("Proc-Type: ")) {
if (!"4,ENCRYPTED".equals(line.substring(11))) { 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: ")) { } else if (line.startsWith("DEK-Info: ")) {
int ptr = line.indexOf(","); int ptr = line.indexOf(",");
if (ptr == -1) { if (ptr == -1) {
throw new IOException("Unrecognized DEK-Info: " + line.substring(10)); throw new FormatException("Unrecognized DEK-Info: " + line.substring(10));
} else { } else {
String algorithm = line.substring(10,ptr); String algorithm = line.substring(10,ptr);
if ("DES-EDE3-CBC".equals(algorithm)) { if ("DES-EDE3-CBC".equals(algorithm)) {
cipher = new TripleDESCBC(); cipher = new TripleDESCBC();
iv = DatatypeConverter.parseHexBinary(line.substring(ptr+1));
} else if ("AES-128-CBC".equals(algorithm)) { } else if ("AES-128-CBC".equals(algorithm)) {
cipher = new AES128CBC(); cipher = new AES128CBC();
iv = DatatypeConverter.parseHexBinary(line.substring(ptr+1));
} else if ("AES-192-CBC".equals(algorithm)) { } else if ("AES-192-CBC".equals(algorithm)) {
cipher = new AES192CBC(); cipher = new AES192CBC();
iv = Base64.decode(line.substring(ptr+1));
} else if ("AES-256-CBC".equals(algorithm)) { } else if ("AES-256-CBC".equals(algorithm)) {
cipher = new AES256CBC(); cipher = new AES256CBC();
iv = Base64.decode(line.substring(ptr+1));
} else { } 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) { } else if (line.length() > 0) {
sb.append(line); sb.append(line);
} }
} }
} }
if (type == null) {
byte[] data = Base64.decode(sb.toString()); throw new FormatException("PKCS5 header not found");
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 (0x30 != data[0]) { ASN1Data asn = new ASN1Data(data = decrypt(Base64.decode(sb.toString()), cipher, iv));
throw new IOException("Failed to decrypt key"); switch(type) {
} case RSA: {
ASN1Data asn = new ASN1Data(data);
if ("RSA".equals(type)) {
KeyFactory factory = KeyFactory.getInstance("RSA"); KeyFactory factory = KeyFactory.getInstance("RSA");
asn.readNext(); asn.readNext();
BigInteger modulus = asn.readNext(); BigInteger modulus = asn.readNext();
@@ -213,7 +204,8 @@ public class PKCS5KeyFile
PublicKey pubKey = factory.generatePublic(new RSAPublicKeySpec(modulus, pubExp)); PublicKey pubKey = factory.generatePublic(new RSAPublicKeySpec(modulus, pubExp));
PrivateKey prvKey = factory.generatePrivate(new RSAPrivateKeySpec(modulus, prvExp)); PrivateKey prvKey = factory.generatePrivate(new RSAPrivateKeySpec(modulus, prvExp));
return new KeyPair(pubKey, prvKey); return new KeyPair(pubKey, prvKey);
} else if ("DSA".equals(type)) { }
case DSA: {
KeyFactory factory = KeyFactory.getInstance("DSA"); KeyFactory factory = KeyFactory.getInstance("DSA");
asn.readNext(); asn.readNext();
BigInteger p = asn.readNext(); BigInteger p = asn.readNext();
@@ -224,8 +216,9 @@ public class PKCS5KeyFile
PublicKey pubKey = factory.generatePublic(new DSAPublicKeySpec(pub, p, q, g)); PublicKey pubKey = factory.generatePublic(new DSAPublicKeySpec(pub, p, q, g));
PrivateKey prvKey = factory.generatePrivate(new DSAPrivateKeySpec(prv, p, q, g)); PrivateKey prvKey = factory.generatePrivate(new DSAPrivateKeySpec(prv, p, q, g));
return new KeyPair(pubKey, prvKey); 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) { } catch (NoSuchAlgorithmException e) {
throw new IOException(e); throw new IOException(e);
@@ -241,15 +234,62 @@ public class PKCS5KeyFile
return "PKCS5KeyFile{resource=" + resource + "}"; 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 { class ASN1Data {
static final byte MAGIC = (byte)0x30;
private byte[] buff; private byte[] buff;
private int index, length; private int index, length;
ASN1Data(byte[] buff) throws IOException { ASN1Data(byte[] buff) throws FormatException {
this.buff = buff; this.buff = buff;
index = 0; index = 0;
if (buff[index++] != (byte)0x30) { if (buff[index++] != MAGIC) {
throw new IOException("Not ASN.1 data"); throw new FormatException("Not ASN.1 data");
} }
length = buff[index++] & 0xff; length = buff[index++] & 0xff;
if ((length & 0x80) != 0) { if ((length & 0x80) != 0) {
@@ -260,7 +300,7 @@ public class PKCS5KeyFile
} }
} }
if ((index + length) > buff.length) { if ((index + length) > buff.length) {
throw new IOException("Length mismatch: " + buff.length + " != " + (index + length)); throw new FormatException("Length mismatch: " + buff.length + " != " + (index + length));
} }
} }

View File

@@ -18,6 +18,8 @@ package net.schmizz.sshj.keyprovider;
import net.schmizz.sshj.common.KeyType; import net.schmizz.sshj.common.KeyType;
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider; import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
import net.schmizz.sshj.userauth.keyprovider.PKCS5KeyFile; 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 net.schmizz.sshj.util.KeyUtil;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -37,6 +39,15 @@ public class PKCS5KeyFileTest {
static final String pubExp = "23"; static final String pubExp = "23";
static final String privExp = "57bcee2e2656eb2c94033d802dd62d726c6705fabad1fd0df86b67600a96431301620d395cbf5871c7af3d3974dfe5c30f5c60d95d7e6e75df69ed6c5a36a9c8aef554b5058b76a719b8478efa08ad1ebf08c8c25fe4b9bc0bfbb9be5d4f60e6213b4ab1c26ad33f5bba7d93e1cd65f65f5a79eb6ebfb32f930a2b0244378b4727acf83b5fb376c38d4abecc5dc3fc399e618e792d4c745d2dbbb9735242e5033129f2985ca3e28fa33cad2afe3e70e1b07ed2b6ec8a3f843fb4bffe3385ad211c6600618488f4ac70397e8eb036b82d811283dc728504cddbe1533c4dd31b1ec99ffa74fd0e3883a9cb3e2315cc1a56f55d38ed40520dd9ec91b4d2dd790d1b"; 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 @Before
public void setUp() public void setUp()
throws UnsupportedEncodingException, GeneralSecurityException { throws UnsupportedEncodingException, GeneralSecurityException {
@@ -56,4 +67,30 @@ public class PKCS5KeyFileTest {
assertEquals(rsa.getType(), KeyType.RSA); 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());
}
} }