/*
* Copyright (c) 2002-2010 Atsuhiko Yamanaka, JCraft,Inc. All rights reserved.
* Copyright (c) 2010-2011 Michael Laudati, N1 Concepts LLC.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. The names of the authors may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
* INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.vngx.jsch.userauth;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import org.vngx.jsch.Buffer;
import org.vngx.jsch.Util;
import org.vngx.jsch.algorithm.AlgorithmManager;
import org.vngx.jsch.algorithm.SignatureDSA;
import org.vngx.jsch.algorithm.SignatureRSA;
import org.vngx.jsch.cipher.Cipher;
import org.vngx.jsch.cipher.CipherManager;
import org.vngx.jsch.algorithm.Algorithms;
import org.vngx.jsch.exception.JSchException;
import org.vngx.jsch.hash.Hash;
import org.vngx.jsch.hash.HashManager;
import org.vngx.jsch.util.DataUtil;
import org.vngx.jsch.util.KeyType;
/**
* Implementation of <code>Identity</code> for an identity key file.
*
* @see org.vngx.jsch.Identity
*
* @author Atsuhiko Yamanaka
* @author Michael Laudati
*/
public class IdentityFile implements Identity {
private static final int OPENSSH = 0;
private static final int FSECURE = 1;
//private static final int PUTTY = 2;
/** Name of identity. */
private final String _identity;
/** MD5 hash function. */
private final Hash _hash;
/** Cipher (3DES-CBC or AES 256) used for encryption. */
private Cipher _cipher;
/** Key value. */
private byte[] _key;
/** Initialization vector for encryption. */
private byte[] _iv;
/** ??? */
private byte[] _encodedData;
/** Key type for identity (RSA or DSA). */
private KeyType _keyType = null;
/** Vendor type for key file (OpenSSH or FSecure). */
private int _vendor = OPENSSH;
private byte[] _publicKeyBlob = null;
private boolean _encrypted = true;
// DSA
private byte[] _pDSA;
private byte[] _qDSA;
private byte[] _gDSA;
private byte[] _pubKeyDSA;
private byte[] _prvKeyDSA;
// RSA
private byte[] _nRSA; // modulus
private byte[] _eRSA; // public exponent
private byte[] _dRSA; // private exponent
/**
* Factory method to create a new instance of <code>IdentityFile</code> for
* the specified private key file and public key file.
*
* @param prvfile
* @param pubfile
* @return identity file instance
* @throws JSchException
*/
public static IdentityFile newInstance(String prvfile, String pubfile) throws JSchException {
byte[] prvkey, pubkey;
FileChannel fc = null;
try { // Attempt to read in private key file
fc = new FileInputStream(prvfile).getChannel();
ByteBuffer bb = ByteBuffer.wrap(prvkey = new byte[(int) fc.size()]);
fc.read(bb);
} catch(Exception e) {
throw new JSchException("Failed to read private key file: "+e, e);
} finally {
if( fc != null ) {
try { fc.close(); } catch(IOException ioe) { /* Ignore error. */ }
fc = null;
}
}
String _pubfile = pubfile;
if( pubfile == null ) {
_pubfile = prvfile + ".pub";
}
try { // Attempt to read in the public key file
fc = new FileInputStream(_pubfile).getChannel();
ByteBuffer bb = ByteBuffer.wrap(pubkey = new byte[(int) fc.size()]);
fc.read(bb);
} catch(Exception e) {
if( pubfile != null ) {
// The pubfile is explicitly given, but not accessible.
throw new JSchException("Failed to read public key file: "+e, e);
}
pubkey = null; // TODO Temp until figured out...
} finally {
if( fc != null ) {
try { fc.close(); } catch(IOException ioe) { /* Ignore error. */ }
fc = null;
}
}
// Create new identity instance and return
return newInstance(prvfile, prvkey, pubkey);
}
/**
* Factory method to create a new instance of <code>IdentityFile</code> for
* the specified name, private key and public key.
*
* @param name
* @param prvkey
* @param pubkey
* @return identity file
* @throws JSchException
*/
public static IdentityFile newInstance(String name, byte[] prvkey, byte[] pubkey) throws JSchException {
try {
return new IdentityFile(name, prvkey, pubkey);
} finally {
Util.bzero(prvkey);
}
}
/**
* Private constructor to prevent direct instantiation. Instances should be
* created using the factory methods <code>newInstance()</code>.
*
* @param name
* @param prvkey
* @param pubkey
* @throws JSchException
*/
private IdentityFile(String name, byte[] prvkey, byte[] pubkey) throws JSchException {
_identity = name;
try {
_cipher = CipherManager.getManager().createCipher(Cipher.CIPHER_3DES_CBC);
_key = new byte[_cipher.getBlockSize()]; // 24
_iv = new byte[_cipher.getIVSize()]; // 8
_hash = HashManager.getManager().createHash(Hash.HASH_MD5);
byte[] buf = prvkey;
int len = buf.length, i = 0;
// Loop past any invalid dash data found in some identity files
while( i < len ) {
if( buf[i] == '-' && i + 4 < len
&& buf[i + 1] == '-' && buf[i + 2] == '-'
&& buf[i + 3] == '-' && buf[i + 4] == '-' ) {
break;
}
i++;
}
while( i < len ) {
if( buf[i] == 'B' && i+3 < len && buf[i + 1] == 'E' && buf[i + 2] == 'G' && buf[i + 3] == 'I' ) {
i += 6;
if( buf[i] == 'D' && buf[i + 1] == 'S' && buf[i + 2] == 'A' ) {
_keyType = KeyType.SSH_DSS;
} else if( buf[i] == 'R' && buf[i + 1] == 'S' && buf[i + 2] == 'A' ) {
_keyType = KeyType.SSH_RSA;
} else if( buf[i] == 'S' && buf[i + 1] == 'S' && buf[i + 2] == 'H' ) { // FSecure
_keyType = KeyType.UNKNOWN;
_vendor = FSECURE;
} else {
throw new JSchException("invalid privatekey: " + _identity);
}
i += 3;
continue;
}
if( buf[i] == 'A' && i+7 < len && buf[i + 1] == 'E' && buf[i + 2] == 'S' && buf[i + 3] == '-'
&& buf[i + 4] == '2' && buf[i + 5] == '5' && buf[i + 6] == '6' && buf[i + 7] == '-' ) {
i += 8;
if( CipherManager.getManager().isSupported(Cipher.CIPHER_AES256_CBC) ) {
_cipher = CipherManager.getManager().createCipher(Cipher.CIPHER_AES256_CBC);
_key = new byte[_cipher.getBlockSize()];
_iv = new byte[_cipher.getIVSize()];
} else {
throw new JSchException("privatekey: aes256-cbc is not available " + _identity);
}
continue;
}
// If AES-192 encryption
if( buf[i] == 'A' && i + 7 < len && buf[i + 1] == 'E' && buf[i + 2] == 'S' && buf[i + 3] == '-'
&& buf[i + 4] == '1' && buf[i + 5] == '9' && buf[i + 6] == '2' && buf[i + 7] == '-' ) {
i += 8;
if( CipherManager.getManager().isSupported(Cipher.CIPHER_AES192_CBC) ) {
_cipher = CipherManager.getManager().createCipher(Cipher.CIPHER_AES192_CBC);
_key = new byte[_cipher.getBlockSize()];
_iv = new byte[_cipher.getIVSize()];
} else {
throw new JSchException("privatekey: aes192-cbc is not available " + _identity);
}
continue;
}
// If AES-128 encryption
if( buf[i] == 'A' && i + 7 < len && buf[i + 1] == 'E' && buf[i + 2] == 'S' && buf[i + 3] == '-'
&& buf[i + 4] == '1' && buf[i + 5] == '2' && buf[i + 6] == '8' && buf[i + 7] == '-' ) {
i += 8;
if( CipherManager.getManager().isSupported(Cipher.CIPHER_AES128_CBC) ) {
_cipher = CipherManager.getManager().createCipher(Cipher.CIPHER_AES128_CBC);
_key = new byte[_cipher.getBlockSize()];
_iv = new byte[_cipher.getIVSize()];
} else {
throw new JSchException("privatekey: aes128-cbc is not available " + _identity);
}
continue;
}
if( buf[i] == 'C' && i+3 < len && buf[i + 1] == 'B' && buf[i + 2] == 'C' && buf[i + 3] == ',' ) {
i += 4;
for( int ii = 0; ii < _iv.length; ii++ ) {
_iv[ii] = (byte) (((a2b(buf[i++]) << 4) & 0xf0) + (a2b(buf[i++]) & 0xf));
}
continue;
}
if( buf[i] == 0x0d && i + 1 < len && buf[i + 1] == 0x0a ) {
i++;
continue;
}
if( buf[i] == 0x0a && i + 1 < len ) {
if( buf[i + 1] == 0x0a ) {
i += 2;
break;
}
if( buf[i + 1] == 0x0d && i + 2 < len && buf[i + 2] == 0x0a ) {
i += 3;
break;
}
boolean inheader = false;
for( int j = i + 1; j < len; j++ ) {
if( buf[j] == 0x0a ) {
break;
}
if( buf[j] == ':' ) {
inheader = true;
break;
}
}
if( !inheader ) {
i++;
_encrypted = false; // no passphrase
break;
}
}
i++;
}
if( _keyType == null ) {
throw new JSchException("invalid privatekey: " + _identity);
}
int start = i;
while( i < len ) {
if( buf[i] == 0x0a ) {
boolean xd = (buf[i - 1] == 0x0d);
System.arraycopy(buf, i + 1, buf, i - (xd ? 1 : 0), len - i - 1 - (xd ? 1 : 0));
if( xd ) {
len--;
}
len--;
continue;
}
if( buf[i] == '-' ) {
break;
}
i++;
}
_encodedData = Util.fromBase64(buf, start, i - start);
if( _encodedData.length > 4 && // FSecure
_encodedData[0] == (byte) 0x3f
&& _encodedData[1] == (byte) 0x6f
&& _encodedData[2] == (byte) 0xf9
&& _encodedData[3] == (byte) 0xeb ) {
Buffer _buf = new Buffer(_encodedData);
_buf.getInt(); // 0x3f6ff9be
_buf.getInt();
@SuppressWarnings("unused")
byte[] typeName = _buf.getString();
byte[] cipherName = _buf.getString();
String cipher = Util.byte2str(cipherName);
if( cipher.equals("3des-cbc") ) {
_buf.getInt();
byte[] foo = new byte[_encodedData.length - _buf.getOffSet()];
_buf.getBytes(foo);
_encodedData = foo;
_encrypted = true;
throw new JSchException("unknown privatekey format: " + _identity);
} else if( cipher.equals("none") ) {
_buf.getInt();
//_buf.getInt();
_encrypted = false;
byte[] foo = new byte[_encodedData.length - _buf.getOffSet()];
_buf.getBytes(foo);
_encodedData = foo;
}
}
if( pubkey == null ) {
return;
}
buf = pubkey;
len = buf.length;
if( buf.length > 4 && // FSecure's public key
buf[0] == '-' && buf[1] == '-' && buf[2] == '-' && buf[3] == '-' ) {
i = 0;
do {
i++;
} while( len > i && buf[i] != 0x0a );
if( len <= i ) {
return;
}
while( i < len ) {
if( buf[i] == 0x0a ) {
boolean inheader = false;
for( int j = i + 1; j < len; j++ ) {
if( buf[j] == 0x0a ) {
break;
}
if( buf[j] == ':' ) {
inheader = true;
break;
}
}
if( !inheader ) {
i++;
break;
}
}
i++;
}
if( len <= i ) {
return;
}
start = i;
while( i < len ) {
if( buf[i] == 0x0a ) {
System.arraycopy(buf, i + 1, buf, i, len - i - 1);
len--;
continue;
}
if( buf[i] == '-' ) {
break;
}
i++;
}
_publicKeyBlob = Util.fromBase64(buf, start, i - start);
if( _keyType == KeyType.UNKNOWN && _publicKeyBlob.length > 8 ) {
if( _publicKeyBlob[8] == 'd' ) {
_keyType = KeyType.SSH_DSS;
} else if( _publicKeyBlob[8] == 'r' ) {
_keyType = KeyType.SSH_RSA;
}
}
} else {
if( buf[0] != 's' || buf[1] != 's' || buf[2] != 'h' || buf[3] != '-' ) {
return;
}
i = 0;
while( i < len ) {
if( buf[i] == ' ' ) {
break;
}
i++;
}
i++;
if( i >= len ) {
return;
}
start = i;
while( i < len ) {
if( buf[i] == ' ' || buf[i] == '\n' ) {
break;
}
i++;
}
_publicKeyBlob = Util.fromBase64(buf, start, i - start);
if( _publicKeyBlob.length < 4 + 7 ) { // It must start with "ssh-XXX".
_publicKeyBlob = null;
}
}
} catch(JSchException e) {
throw e;
} catch(Exception e) {
throw new JSchException("Failed to create IdentityFile instance: "+e, e);
}
}
@Override
public String getAlgorithmName() {
return _keyType.toString();
}
@Override
public boolean setPassphrase(byte[] passphrase) throws JSchException {
/*
hash is MD5
h(0) <- hash(passphrase, iv);
h(n) <- hash(h(n-1), passphrase, iv);
key <- (h(0),...,h(n))[0,..,key.length];
*/
try {
if( _encrypted ) {
if( passphrase == null ) {
return false;
}
int hsize = _hash.getBlockSize();
byte[] hn = new byte[_key.length / hsize * hsize + (_key.length % hsize == 0 ? 0 : hsize)];
byte[] tmp = null;
if( _vendor == OPENSSH ) {
for( int index = 0; index + hsize <= hn.length; ) {
if( tmp != null ) {
_hash.update(tmp, 0, tmp.length);
}
_hash.update(passphrase, 0, passphrase.length);
_hash.update(_iv, 0, _iv.length > 8 ? 8 : _iv.length);
tmp = _hash.digest();
System.arraycopy(tmp, 0, hn, index, tmp.length);
index += tmp.length;
}
System.arraycopy(hn, 0, _key, 0, _key.length);
} else if( _vendor == FSECURE ) {
for( int index = 0; index + hsize <= hn.length; ) {
if( tmp != null ) {
_hash.update(tmp, 0, tmp.length);
}
_hash.update(passphrase, 0, passphrase.length);
tmp = _hash.digest();
System.arraycopy(tmp, 0, hn, index, tmp.length);
index += tmp.length;
}
System.arraycopy(hn, 0, _key, 0, _key.length);
}
}
if( decrypt() ) {
_encrypted = false;
return true;
}
_pDSA = _qDSA = _gDSA = _pubKeyDSA = _prvKeyDSA = null;
return false;
} catch(Exception e) {
throw new JSchException("Failed to set passphrase: "+e, e);
} finally {
Util.bzero(passphrase);
}
}
@Override
public byte[] getPublicKeyBlob() {
if( _publicKeyBlob != null ) {
return _publicKeyBlob;
}
// Generate public key blob based on key type
byte[] keyBlob = null;
switch( _keyType ) {
case SSH_RSA:
if( _eRSA == null ) { return null; }
keyBlob = new byte[KeyType.SSH_RSA.toString().length() + 4
+ _eRSA.length + 4
+ _nRSA.length + 4];
Buffer rsaBuf = new Buffer(keyBlob);
rsaBuf.putString(KeyType.SSH_RSA.getBytes());
rsaBuf.putString(_eRSA);
rsaBuf.putString(_nRSA);
return keyBlob;
case SSH_DSS:
if( _pDSA == null ) { return null; }
keyBlob = new byte[KeyType.SSH_DSS.toString().length() + 4
+ _pDSA.length + 4
+ _qDSA.length + 4
+ _gDSA.length + 4
+ _pubKeyDSA.length + 4];
Buffer dsaBuf = new Buffer(keyBlob);
dsaBuf.putString(KeyType.SSH_DSS.getBytes());
dsaBuf.putString(_pDSA);
dsaBuf.putString(_qDSA);
dsaBuf.putString(_gDSA);
dsaBuf.putString(_pubKeyDSA);
return keyBlob;
default:
throw new IllegalStateException("Failed to generate public key blob, invalid key type: "+_keyType);
}
}
@Override
public byte[] getSignature(byte[] data) {
switch( _keyType ) {
case SSH_RSA: {
try {
SignatureRSA rsa = AlgorithmManager.getManager().createAlgorithm(Algorithms.SIGNATURE_RSA);
rsa.setPrvKey(_dRSA, _nRSA);
rsa.update(data);
byte[] sig = rsa.sign();
byte[] buffer = new byte[KeyType.SSH_RSA.toString().length() + 4 + sig.length + 4];
Buffer buf = new Buffer(buffer);
buf.putString(KeyType.SSH_RSA.getBytes());
buf.putString(sig);
return buffer;
} catch(Exception e) {
// TODO Error handling?
}
return null;
}
case SSH_DSS: {
try {
SignatureDSA dsa = AlgorithmManager.getManager().createAlgorithm(Algorithms.SIGNATURE_DSS);
dsa.setPrvKey(_prvKeyDSA, _pDSA, _qDSA, _gDSA);
dsa.update(data);
byte[] sig = dsa.sign();
byte[] buffer = new byte[KeyType.SSH_DSS.toString().length() + 4 + sig.length + 4];
Buffer buf = new Buffer(buffer);
buf.putString(KeyType.SSH_DSS.getBytes());
buf.putString(sig);
return buffer;
} catch(Exception e) {
// TODO Error handling?
}
return null;
}
default:
throw new IllegalStateException("Failed to get signature, invalid key type: "+_keyType);
}
}
@Override
public boolean decrypt() {
switch( _keyType ) {
case SSH_RSA: return decryptRSA();
case SSH_DSS: return decryptDSS();
default: throw new IllegalStateException("Failed to decrypt, invalid key type: "+_keyType);
}
}
boolean decryptRSA() {
try {
byte[] plain;
if( _encrypted ) {
if( _vendor == OPENSSH ) {
_cipher.init(Cipher.DECRYPT_MODE, _key, _iv);
plain = new byte[_encodedData.length];
_cipher.update(_encodedData, 0, _encodedData.length, plain, 0);
} else if( _vendor == FSECURE ) {
for( int i = 0; i < _iv.length; i++ ) {
_iv[i] = 0;
}
_cipher.init(Cipher.DECRYPT_MODE, _key, _iv);
plain = new byte[_encodedData.length];
_cipher.update(_encodedData, 0, _encodedData.length, plain, 0);
} else {
return false;
}
} else {
if( _nRSA != null ) {
return true;
}
plain = _encodedData;
}
if( _vendor == FSECURE ) { // FSecure
Buffer buf = new Buffer(plain);
int foo = buf.getInt();
if( plain.length != foo + 4 ) {
return false;
}
_eRSA = buf.getMPIntBits();
_dRSA = buf.getMPIntBits();
_nRSA = buf.getMPIntBits();
buf.getMPIntBits(); // u_array
buf.getMPIntBits(); // p_array
buf.getMPIntBits(); // q_array
return true;
}
int[] index = new int[1];
int length = 0;
if( plain[index[0]] != 0x30 ) {
return false;
}
index[0]++; // SEQUENCE
length = plain[index[0]++] & 0xff;
if( (length & 0x80) != 0 ) {
int foo = length & 0x7f;
length = 0;
while( foo-- > 0 ) {
length = (length << 8) + (plain[index[0]++] & 0xff);
}
}
if( plain[index[0]] != 0x02 ) {
return false;
}
DataUtil.readINTEGER(index, plain);
_nRSA = DataUtil.readINTEGER(index, plain);
_eRSA = DataUtil.readINTEGER(index, plain);
_dRSA = DataUtil.readINTEGER(index, plain);
DataUtil.readINTEGER(index, plain); // p_array
DataUtil.readINTEGER(index, plain); // q_array
DataUtil.readINTEGER(index, plain); // dmp1_array
DataUtil.readINTEGER(index, plain); // dmq1_array
DataUtil.readINTEGER(index, plain); // iqmp_array
} catch(Exception e) {
// TODO Error handling?
return false;
}
return true;
}
boolean decryptDSS() {
try {
byte[] plain;
if( _encrypted ) {
if( _vendor == OPENSSH ) {
_cipher.init(Cipher.DECRYPT_MODE, _key, _iv);
plain = new byte[_encodedData.length];
_cipher.update(_encodedData, 0, _encodedData.length, plain, 0);
} else if( _vendor == FSECURE ) {
for( int i = 0; i < _iv.length; i++ ) {
_iv[i] = 0;
}
_cipher.init(Cipher.DECRYPT_MODE, _key, _iv);
plain = new byte[_encodedData.length];
_cipher.update(_encodedData, 0, _encodedData.length, plain, 0);
} else {
return false;
}
} else {
if( _pDSA != null ) {
return true;
}
plain = _encodedData;
}
if( _vendor == FSECURE ) { // FSecure
Buffer buf = new Buffer(plain);
int foo = buf.getInt();
if( plain.length != foo + 4 ) {
return false;
}
_pDSA = buf.getMPIntBits();
_gDSA = buf.getMPIntBits();
_qDSA = buf.getMPIntBits();
_pubKeyDSA = buf.getMPIntBits();
_prvKeyDSA = buf.getMPIntBits();
return true;
}
int[] index = new int[1];
int length = 0;
if( plain[index[0]] != 0x30 ) {
return false;
}
index[0]++; // SEQUENCE
length = plain[index[0]++] & 0xff;
if( (length & 0x80) != 0 ) {
int foo = length & 0x7f;
length = 0;
while( foo-- > 0 ) {
length = (length << 8) + (plain[index[0]++] & 0xff);
}
}
if( plain[index[0]] != 0x02 ) {
return false;
}
DataUtil.readINTEGER(index, plain);
_pDSA = DataUtil.readINTEGER(index, plain);
_qDSA = DataUtil.readINTEGER(index, plain);
_gDSA = DataUtil.readINTEGER(index, plain);
_pubKeyDSA = DataUtil.readINTEGER(index, plain);
_prvKeyDSA = DataUtil.readINTEGER(index, plain);
} catch(Exception e) {
// TODO Error handling?
return false;
}
return true;
}
@Override
public boolean isEncrypted() {
return _encrypted;
}
@Override
public String getName() {
return _identity;
}
@Override
public void clear() {
Util.bzero(_encodedData);
Util.bzero(_prvKeyDSA);
Util.bzero(_dRSA);
Util.bzero(_key);
Util.bzero(_iv);
}
private static byte a2b(byte c) {
if( '0' <= c && c <= '9' ) {
return (byte) (c - '0');
}
if( 'a' <= c && c <= 'z' ) {
return (byte) (c - 'a' + 10);
}
return (byte) (c - 'A' + 10);
}
@Override
protected void finalize() throws Throwable {
clear();
super.finalize();
}
@Override
public int hashCode() {
return getName().hashCode();
}
@Override
public boolean equals(Object o) {
return o == this || (o instanceof IdentityFile && getName().equals(((IdentityFile) o).getName()));
}
}