mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-06 15:20:54 +03:00
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:
@@ -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 {
|
||||
type = "UNKNOWN";
|
||||
throw new FormatException("Unrecognized PKCS5 key type: " + s);
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
if (type == null) {
|
||||
throw new FormatException("PKCS5 header not found");
|
||||
}
|
||||
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]) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user