From 62b87268071e33da408a9bd041bb64001c027714 Mon Sep 17 00:00:00 2001 From: David Solin Date: Fri, 19 Aug 2016 12:18:49 -0500 Subject: [PATCH] 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. --- .../userauth/keyprovider/PKCS5KeyFile.java | 158 +++++++++++------- 1 file changed, 98 insertions(+), 60 deletions(-) diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS5KeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS5KeyFile.java index 736b78da..6acb520e 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS5KeyFile.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS5KeyFile.java @@ -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)); } }