Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--hudson-core/src/main/java/hudson/util/ssh/DEREncoder.java117
-rw-r--r--hudson-core/src/main/java/hudson/util/ssh/KeyReader.java65
-rw-r--r--hudson-core/src/main/java/hudson/util/ssh/PuTTYKey.java276
3 files changed, 458 insertions, 0 deletions
diff --git a/hudson-core/src/main/java/hudson/util/ssh/DEREncoder.java b/hudson-core/src/main/java/hudson/util/ssh/DEREncoder.java
new file mode 100644
index 00000000..b04bebdd
--- /dev/null
+++ b/hudson-core/src/main/java/hudson/util/ssh/DEREncoder.java
@@ -0,0 +1,117 @@
+/*******************************************************************************
+ *
+ * Copyright (c) 2004-2010 Oracle Corporation.
+ *
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *
+ * Kohsuke Kawaguchi
+ *
+ *******************************************************************************/
+
+package hudson.util.ssh;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.ByteArrayOutputStream;
+import java.math.BigInteger;
+import org.apache.commons.codec.binary.Base64;
+
+/**
+ * ASN.1 DER encoder.
+ *
+ * This is the binary packaging format used by OpenSSH key.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+final class DEREncoder {
+
+ private final DataOutputStream out;
+ private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ public DEREncoder() {
+ this.out = new DataOutputStream(baos);
+ }
+
+ public void reset() {
+ baos.reset();
+ }
+
+ public byte[] toByteArray() {
+ return baos.toByteArray();
+ }
+
+ /**
+ * Converts that to base64 with new lines to make it 64-chars per line.
+ */
+ public String toBase64() {
+ byte[] r = Base64.encodeBase64(toByteArray());
+
+ StringBuilder buf = new StringBuilder();
+ for (int i = 0; i < r.length; i++) {
+ buf.append(r[i]);
+ if (i % 64 == 63) {
+ buf.append('\n');
+ }
+ }
+ if (r.length % 64 != 0) {
+ buf.append('\n');
+ }
+ return buf.toString();
+ }
+
+ public DEREncoder writeSequence(byte[] data, int offset, int length) throws IOException {
+ out.write(0x30);
+ writeLength(length);
+ out.write(data, offset, length);
+ return this;
+ }
+
+ public DEREncoder writeSequence(byte[] data) throws IOException {
+ return writeSequence(data, 0, data.length);
+ }
+
+ public void writeLength(int len) throws IOException {
+ if (len < 0x80) {
+ // short form
+ out.write(len);
+ return;
+ }
+
+ // how many bytes do we need to store this length?
+ int bytes = countByteLen(len);
+
+ out.write(0x80 | bytes);
+ for (int i = bytes - 1; i >= 0; i--) {
+ out.write((len >> (8 * i)) & 0xFF);
+ }
+ }
+
+ private int countByteLen(int len) {
+ int bytes = 0;
+ while (len > 0) {
+ bytes++;
+ len >>= 8;
+ }
+ return bytes;
+ }
+
+ public DEREncoder write(BigInteger i) throws IOException {
+ out.write(0x02);
+ byte[] bytes = i.toByteArray();
+ writeLength(bytes.length);
+ out.write(bytes);
+ return this;
+ }
+
+ public DEREncoder write(BigInteger... ints) throws IOException {
+ for (BigInteger i : ints) {
+ write(i);
+ }
+ return this;
+ }
+}
diff --git a/hudson-core/src/main/java/hudson/util/ssh/KeyReader.java b/hudson-core/src/main/java/hudson/util/ssh/KeyReader.java
new file mode 100644
index 00000000..b8d971fa
--- /dev/null
+++ b/hudson-core/src/main/java/hudson/util/ssh/KeyReader.java
@@ -0,0 +1,65 @@
+/********************************************************************************
+ *
+ * Copyright (c) 2004-2010 Oracle Corporation.
+ *
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *
+ * Kohsuke Kawaguchi
+ *
+ *******************************************************************************/
+
+package hudson.util.ssh;
+
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+
+/**
+ * Parses the putty key bit vector, which is an encoded sequence of {@link BigInteger}s.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+class KeyReader {
+
+ private final DataInput di;
+
+ KeyReader(byte[] key) {
+ this.di = new DataInputStream(new ByteArrayInputStream(key));
+ }
+
+ /**
+ * Skips an integer without reading it.
+ */
+ public void skip() {
+ try {
+ di.skipBytes(di.readInt());
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private byte[] read() {
+ try {
+ int len = di.readInt();
+ byte[] r = new byte[len];
+ di.readFully(r);
+ return r;
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Reads the next integer.
+ */
+ public BigInteger readInt() {
+ return new BigInteger(read());
+ }
+}
diff --git a/hudson-core/src/main/java/hudson/util/ssh/PuTTYKey.java b/hudson-core/src/main/java/hudson/util/ssh/PuTTYKey.java
new file mode 100644
index 00000000..ddfa2d4a
--- /dev/null
+++ b/hudson-core/src/main/java/hudson/util/ssh/PuTTYKey.java
@@ -0,0 +1,276 @@
+/**
+ * *****************************************************************************
+ *
+ * Copyright (c) 2004-2010 Oracle Corporation.
+ *
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *
+ * Kohsuke Kawaguchi
+ *
+ ******************************************************************************
+ */
+package hudson.util.ssh;
+
+import ch.ethz.ssh2.crypto.cipher.AES;
+import ch.ethz.ssh2.crypto.cipher.CBCMode;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.FileWriter;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.commons.codec.binary.Base64;
+
+/**
+ * Interprets PuTTY's ".ppk" file.
+ *
+ * <h2>Notes</h2> <ol> <li> The file appears to be a text file but it doesn't
+ * have the fixed encoding. So we just use the platform default encoding, which
+ * is what PuTTY seems to use. Fortunately, the important part is all ASCII, so
+ * this shouldn't really hurt the interpretation of the key. </ol>
+ *
+ * <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>
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public class PuTTYKey {
+
+ private static final String PUTTY_SIGNATURE = "PuTTY-User-Key-File-";
+ private final byte[] privateKey;
+ private final byte[] publicKey;
+ /**
+ * 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>();
+
+ public PuTTYKey(File ppkFile, String passphrase) throws IOException {
+ this(new FileReader(ppkFile), passphrase);
+ }
+
+ public PuTTYKey(InputStream in, String passphrase) throws IOException {
+ this(new InputStreamReader(in), passphrase);
+ }
+
+ public PuTTYKey(Reader in, String passphrase) throws IOException {
+ BufferedReader r = new BufferedReader(in);
+
+ Map<String, String> payload = new HashMap<String, String>();
+
+ // 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 {
+ s += line;
+ }
+ payload.put(headerName, s);
+ }
+ }
+ } finally {
+ r.close();
+ }
+
+ boolean encrypted = "aes256-cbc".equals(headers.get("Encryption"));
+
+ publicKey = decodeBase64(payload.get("Public-Lines"));
+ byte[] privateLines = decodeBase64(payload.get("Private-Lines"));
+
+ if (encrypted) {
+ AES aes = new AES();
+ byte[] key = toKey(passphrase);
+ aes.init(false, key);
+ CBCMode cbc = new CBCMode(aes, new byte[16], false); // initial vector=0
+
+ byte[] out = new byte[privateLines.length];
+ for (int i = 0; i < privateLines.length / cbc.getBlockSize(); i++) {
+ cbc.transformBlock(privateLines, i * cbc.getBlockSize(), out, i * cbc.getBlockSize());
+ }
+ privateLines = out;
+ }
+
+ this.privateKey = privateLines;
+ }
+
+ /**
+ * Key type. Either "ssh-rsa" for RSA key, or "ssh-dss" for DSA key.
+ */
+ public String getAlgorithm() {
+ return headers.get("PuTTY-User-Key-File-2");
+ }
+
+ /**
+ * Converts a passphrase into a key, by following the convention that PuTTY
+ * uses.
+ *
+ * <p> This is used to decrypt the private key when it's encrypted.
+ */
+ private byte[] toKey(String passphrase) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+
+ digest.update(new byte[]{0, 0, 0, 0});
+ digest.update(passphrase.getBytes());
+ byte[] key1 = digest.digest();
+
+ 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 AssertionError(e); // impossible
+ }
+ }
+
+ private static byte[] decodeBase64(String s) throws IOException {
+ return Base64.decodeBase64(s);
+ }
+
+ /**
+ * Converts this key into OpenSSH format.
+ *
+ * @return A multi-line string that can be written back to a file.
+ */
+ public String toOpenSSH() throws IOException {
+ if (getAlgorithm().equals("ssh-rsa")) {
+ KeyReader r = new KeyReader(publicKey);
+ r.skip(); // skip this
+ BigInteger e = r.readInt();
+ BigInteger n = r.readInt();
+
+ r = new KeyReader(privateKey);
+ BigInteger d = r.readInt();
+ BigInteger p = r.readInt();
+ BigInteger q = r.readInt();
+ BigInteger iqmp = r.readInt();
+
+ BigInteger dmp1 = d.mod(p.subtract(BigInteger.ONE));
+ BigInteger dmq1 = d.mod(q.subtract(BigInteger.ONE));
+
+
+ DEREncoder payload = new DEREncoder().writeSequence(
+ new DEREncoder().write(BigInteger.ZERO, n, e, d, p, q, dmp1, dmq1, iqmp).toByteArray());
+
+ StringBuilder buf = new StringBuilder();
+ buf.append("-----BEGIN RSA PRIVATE KEY-----\n");
+ buf.append(payload.toBase64());
+ buf.append("-----END RSA PRIVATE KEY-----\n");
+
+ // debug assist
+ // Object o = PEMDecoder.decode(buf.toString().toCharArray(), null);
+
+ return buf.toString();
+ }
+
+ if (getAlgorithm().equals("ssh-dss")) {
+ KeyReader r = new KeyReader(publicKey);
+ r.skip(); // skip this
+ BigInteger p = r.readInt();
+ BigInteger q = r.readInt();
+ BigInteger g = r.readInt();
+ BigInteger y = r.readInt();
+
+ r = new KeyReader(privateKey);
+ BigInteger x = r.readInt();
+
+ DEREncoder payload = new DEREncoder().writeSequence(
+ new DEREncoder().write(BigInteger.ZERO, p, q, g, y, x).toByteArray());
+
+ StringBuilder buf = new StringBuilder();
+ buf.append("-----BEGIN DSA PRIVATE KEY-----\n");
+ buf.append(payload.toBase64());
+ buf.append("-----END DSA PRIVATE KEY-----\n");
+
+ // debug assist
+ // Object o = PEMDecoder.decode(buf.toString().toCharArray(), null);
+
+ return buf.toString();
+ }
+
+ throw new IllegalArgumentException("Unrecognized key type: " + getAlgorithm());
+ }
+
+ /**
+ * Converts the key to OpenSSH format, then write it to a file.
+ */
+ public void toOpenSSH(File f) throws IOException {
+ FileWriter w = new FileWriter(f);
+ try {
+ w.write(toOpenSSH());
+ } finally {
+ w.close();
+ }
+ }
+
+ /**
+ * Checks if the given file is a PuTTY's ".ppk" file, by looking at the file
+ * contents.
+ */
+ public static boolean isPuTTYKeyFile(File ppkFile) throws IOException {
+ return isPuTTYKeyFile(new FileReader(ppkFile));
+ }
+
+ public static boolean isPuTTYKeyFile(InputStream in) throws IOException {
+ return isPuTTYKeyFile(new InputStreamReader(in));
+ }
+
+ public static boolean isPuTTYKeyFile(Reader _reader) throws IOException {
+ BufferedReader r = new BufferedReader(_reader);
+ try {
+ String line;
+ while ((line = r.readLine()) != null) {
+ if (line.startsWith(PUTTY_SIGNATURE)) {
+ return true;
+ }
+ }
+ return false;
+ } finally {
+ r.close();
+ }
+ }
+}

Back to the top