/* * Copyright (C)2009 - SSHJ Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.schmizz.sshj.userauth.keyprovider; import java.io.*; import java.math.BigInteger; import java.security.*; import java.security.spec.*; import java.util.HashMap; import java.util.Map; import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.bouncycastle.util.encoders.Hex; import net.schmizz.sshj.common.Base64; import net.schmizz.sshj.common.KeyType; import net.schmizz.sshj.userauth.password.PasswordUtils; /** * <h2>Sample PuTTY file format</h2> * <pre> * PuTTY-User-Key-File-2: ssh-rsa * Encryption: none * Comment: rsa-key-20080514 * Public-Lines: 4 * AAAAB3NzaC1yc2EAAAABJQAAAIEAiPVUpONjGeVrwgRPOqy3Ym6kF/f8bltnmjA2 * BMdAtaOpiD8A2ooqtLS5zWYuc0xkW0ogoKvORN+RF4JI+uNUlkxWxnzJM9JLpnvA * HrMoVFaQ0cgDMIHtE1Ob1cGAhlNInPCRnGNJpBNcJ/OJye3yt7WqHP4SPCCLb6nL * nmBUrLM= * Private-Lines: 8 * AAAAgGtYgJzpktzyFjBIkSAmgeVdozVhgKmF6WsDMUID9HKwtU8cn83h6h7ug8qA * hUWcvVxO201/vViTjWVz9ALph3uMnpJiuQaaNYIGztGJBRsBwmQW9738pUXcsUXZ * 79KJP01oHn6Wkrgk26DIOsz04QOBI6C8RumBO4+F1WdfueM9AAAAQQDmA4hcK8Bx * nVtEpcF310mKD3nsbJqARdw5NV9kCxPnEsmy7Sy1L4Ob/nTIrynbc3MA9HQVJkUz * 7V0va5Pjm/T7AAAAQQCYbnG0UEekwk0LG1Hkxh1OrKMxCw2KWMN8ac3L0LVBg/Tk * 8EnB2oT45GGeJaw7KzdoOMFZz0iXLsVLNUjNn2mpAAAAQQCN6SEfWqiNzyc/w5n/ * lFVDHExfVUJp0wXv+kzZzylnw4fs00lC3k4PZDSsb+jYCMesnfJjhDgkUA0XPyo8 * Emdk * Private-MAC: 50c45751d18d74c00fca395deb7b7695e3ed6f77 * </pre> * * @version $Id:$ */ public class PuTTYKeyFile extends BaseFileKeyProvider { public static class Factory implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> { @Override public FileKeyProvider create() { return new PuTTYKeyFile(); } @Override public String getName() { return "PuTTY"; } } private byte[] privateKey; private byte[] publicKey; /** * Key type. Either "ssh-rsa" for RSA key, or "ssh-dss" for DSA key. */ @Override public KeyType getType() throws IOException { return KeyType.fromString(headers.get("PuTTY-User-Key-File-2")); } public boolean isEncrypted() { // Currently the only supported encryption types are "aes256-cbc" and "none". return "aes256-cbc".equals(headers.get("Encryption")); } private Map<String, String> payload = new HashMap<String, String>(); /** * For each line that looks like "Xyz: vvv", it will be stored in this map. */ private final Map<String, String> headers = new HashMap<String, String>(); protected KeyPair readKeyPair() throws IOException { this.parseKeyPair(); if (KeyType.RSA.equals(this.getType())) { final KeyReader publicKeyReader = new KeyReader(publicKey); publicKeyReader.skip(); // skip this // public key exponent BigInteger e = publicKeyReader.readInt(); // modulus BigInteger n = publicKeyReader.readInt(); final KeyReader privateKeyReader = new KeyReader(privateKey); // private key exponent BigInteger d = privateKeyReader.readInt(); final KeyFactory factory; try { factory = KeyFactory.getInstance("RSA"); } catch (NoSuchAlgorithmException s) { throw new IOException(s.getMessage(), s); } try { return new KeyPair( factory.generatePublic(new RSAPublicKeySpec(n, e)), factory.generatePrivate(new RSAPrivateKeySpec(n, d)) ); } catch (InvalidKeySpecException i) { throw new IOException(i.getMessage(), i); } } if (KeyType.DSA.equals(this.getType())) { final KeyReader publicKeyReader = new KeyReader(publicKey); publicKeyReader.skip(); // skip this BigInteger p = publicKeyReader.readInt(); BigInteger q = publicKeyReader.readInt(); BigInteger g = publicKeyReader.readInt(); BigInteger y = publicKeyReader.readInt(); final KeyReader privateKeyReader = new KeyReader(privateKey); // Private exponent from the private key BigInteger x = privateKeyReader.readInt(); final KeyFactory factory; try { factory = KeyFactory.getInstance("DSA"); } catch (NoSuchAlgorithmException s) { throw new IOException(s.getMessage(), s); } try { return new KeyPair( factory.generatePublic(new DSAPublicKeySpec(y, p, q, g)), factory.generatePrivate(new DSAPrivateKeySpec(x, p, q, g)) ); } catch (InvalidKeySpecException e) { throw new IOException(e.getMessage(), e); } } else { throw new IOException(String.format("Unknown key type %s", this.getType())); } } protected void parseKeyPair() throws IOException { BufferedReader r = new BufferedReader(resource.getReader()); // Parse the text into headers and payloads try { String headerName = null; String line; while ((line = r.readLine()) != null) { int idx = line.indexOf(": "); if (idx > 0) { headerName = line.substring(0, idx); headers.put(headerName, line.substring(idx + 2)); } else { String s = payload.get(headerName); if (s == null) { s = line; } else { // Append to previous line s += line; } // Save payload payload.put(headerName, s); } } } finally { r.close(); } // Retrieve keys from payload publicKey = Base64.decode(payload.get("Public-Lines")); if (this.isEncrypted()) { final char[] passphrase; if (pwdf != null) { passphrase = pwdf.reqPassword(resource); } else { passphrase = "".toCharArray(); } try { privateKey = this.decrypt(Base64.decode(payload.get("Private-Lines")), new String(passphrase)); this.verify(new String(passphrase)); } finally { PasswordUtils.blankOut(passphrase); } } else { privateKey = Base64.decode(payload.get("Private-Lines")); } } /** * Converts a passphrase into a key, by following the convention that PuTTY uses. * <p/> * <p/> * This is used to decrypt the private key when it's encrypted. */ private byte[] toKey(final String passphrase) throws IOException { try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); // The encryption key is derived from the passphrase by means of a succession of SHA-1 hashes. // Sequence number 0 digest.update(new byte[]{0, 0, 0, 0}); digest.update(passphrase.getBytes()); byte[] key1 = digest.digest(); // Sequence number 1 digest.update(new byte[]{0, 0, 0, 1}); digest.update(passphrase.getBytes()); byte[] key2 = digest.digest(); byte[] r = new byte[32]; System.arraycopy(key1, 0, r, 0, 20); System.arraycopy(key2, 0, r, 20, 12); return r; } catch (NoSuchAlgorithmException e) { throw new IOException(e.getMessage(), e); } } /** * Verify the MAC. */ private void verify(final String passphrase) throws IOException { try { // The key to the MAC is itself a SHA-1 hash of: MessageDigest digest = MessageDigest.getInstance("SHA-1"); digest.update("putty-private-key-file-mac-key".getBytes()); if (passphrase != null) { digest.update(passphrase.getBytes()); } final byte[] key = digest.digest(); final Mac mac = Mac.getInstance("HmacSHA1"); mac.init(new SecretKeySpec(key, 0, 20, mac.getAlgorithm())); final ByteArrayOutputStream out = new ByteArrayOutputStream(); final DataOutputStream data = new DataOutputStream(out); // name of algorithm data.writeInt(this.getType().toString().length()); data.writeBytes(this.getType().toString()); data.writeInt(headers.get("Encryption").length()); data.writeBytes(headers.get("Encryption")); data.writeInt(headers.get("Comment").length()); data.writeBytes(headers.get("Comment")); data.writeInt(publicKey.length); data.write(publicKey); data.writeInt(privateKey.length); data.write(privateKey); final String encoded = Hex.toHexString(mac.doFinal(out.toByteArray())); final String reference = headers.get("Private-MAC"); if (!encoded.equals(reference)) { throw new IOException("Invalid passphrase"); } } catch (GeneralSecurityException e) { throw new IOException(e.getMessage(), e); } } /** * Decrypt private key * * @param passphrase To decrypt */ private byte[] decrypt(final byte[] key, final String passphrase) throws IOException { try { final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); final byte[] expanded = this.toKey(passphrase); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(expanded, 0, 32, "AES"), new IvParameterSpec(new byte[16])); // initial vector=0 return cipher.doFinal(key); } catch (GeneralSecurityException e) { throw new IOException(e.getMessage(), e); } } /** * Parses the putty key bit vector, which is an encoded sequence * of {@link java.math.BigInteger}s. */ private final static class KeyReader { private final DataInput di; public KeyReader(byte[] key) { this.di = new DataInputStream(new ByteArrayInputStream(key)); } /** * Skips an integer without reading it. */ public void skip() throws IOException { final int read = di.readInt(); if (read != di.skipBytes(read)) { throw new IOException(String.format("Failed to skip %d bytes", read)); } } private byte[] read() throws IOException { int len = di.readInt(); if (len <= 0 || len > 513) { throw new IOException(String.format("Invalid length %d", len)); } byte[] r = new byte[len]; di.readFully(r); return r; } /** * Reads the next integer. */ public BigInteger readInt() throws IOException { return new BigInteger(read()); } } }