Fixed bug wherein PKCS5KeyFile messed up the data if a bad passphrase was used, which prevented decryption with a subsequent correct passphrase.

Also, made it possible for subclasses of PKCS5KeyFile to access the decrypted ASN.1 data directly (useful if you want to decrypt then store a PKCS5 key for later use elsewhere).
And I think I made the code a little prettier.
This commit is contained in:
David Solin
2016-08-19 12:18:49 -05:00
parent 1caa7ac722
commit 62b8726807

View File

@@ -27,7 +27,6 @@ import net.schmizz.sshj.transport.cipher.TripleDESCBC;
import net.schmizz.sshj.transport.digest.Digest;
import net.schmizz.sshj.transport.digest.MD5;
import net.schmizz.sshj.userauth.password.PasswordFinder;
import net.schmizz.sshj.userauth.password.PasswordUtils;
import net.schmizz.sshj.userauth.password.PrivateKeyFileResource;
import net.schmizz.sshj.userauth.password.PrivateKeyReaderResource;
import net.schmizz.sshj.userauth.password.PrivateKeyStringResource;
@@ -77,8 +76,7 @@ public class PKCS5KeyFile
protected KeyPair kp;
protected KeyType type;
protected char[] passphrase; // for blanking out
protected byte[] data;
@Override
public PrivateKey getPrivate()
@@ -139,7 +137,6 @@ public class PKCS5KeyFile
throws IOException {
BufferedReader reader = new BufferedReader(resource.getReader());
try {
String type = null;
String line = null;
Cipher cipher = new NoneCipher();
StringBuffer sb = new StringBuffer();
@@ -148,37 +145,46 @@ public class PKCS5KeyFile
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);
if ("DES-EDE3-CBC".equals(algorithm)) {
cipher = new TripleDESCBC();
iv = DatatypeConverter.parseHexBinary(line.substring(ptr+1));
iv = Arrays.copyOfRange(DatatypeConverter.parseHexBinary(line.substring(ptr+1)), 0, cipher.getIVSize());
} else if ("AES-128-CBC".equals(algorithm)) {
cipher = new AES128CBC();
iv = DatatypeConverter.parseHexBinary(line.substring(ptr+1));
iv = Arrays.copyOfRange(DatatypeConverter.parseHexBinary(line.substring(ptr+1)), 0, cipher.getIVSize());
} else if ("AES-192-CBC".equals(algorithm)) {
cipher = new AES192CBC();
iv = Base64.decode(line.substring(ptr+1));
iv = Arrays.copyOfRange(Base64.decode(line.substring(ptr+1)), 0, cipher.getIVSize());
} else if ("AES-256-CBC".equals(algorithm)) {
cipher = new AES256CBC();
iv = Base64.decode(line.substring(ptr+1));
iv = Arrays.copyOfRange(Base64.decode(line.substring(ptr+1)), 0, cipher.getIVSize());
} else {
throw new IOException("Not a supported algorithm: " + algorithm);
throw new FormatException("Not a supported algorithm: " + algorithm);
}
}
} else if (line.length() > 0) {
@@ -186,47 +192,12 @@ public class PKCS5KeyFile
}
}
}
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();
@@ -235,7 +206,8 @@ public class PKCS5KeyFile
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();
@@ -246,8 +218,9 @@ public class PKCS5KeyFile
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);
@@ -263,15 +236,80 @@ public class PKCS5KeyFile
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 > 8 ? 8 : 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");
}
/**
* 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);
}
}
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) {
@@ -282,7 +320,7 @@ public class PKCS5KeyFile
}
}
if ((index + length) > buff.length) {
throw new IOException("Length mismatch: " + buff.length + " != " + (index + length));
throw new FormatException("Length mismatch: " + buff.length + " != " + (index + length));
}
}