|
1 | 1 | package com.laytonsmith.PureUtilities.Common; |
2 | 2 |
|
3 | | -import java.io.ByteArrayInputStream; |
4 | 3 | import java.io.ByteArrayOutputStream; |
| 4 | +import java.io.DataOutputStream; |
5 | 5 | import java.io.IOException; |
6 | | -import java.io.ObjectInputStream; |
7 | | -import java.io.ObjectOutputStream; |
| 6 | +import java.nio.ByteBuffer; |
8 | 7 | import java.security.InvalidKeyException; |
9 | 8 | import java.security.Key; |
| 9 | +import java.security.KeyFactory; |
10 | 10 | import java.security.KeyPair; |
11 | 11 | import java.security.KeyPairGenerator; |
12 | 12 | import java.security.NoSuchAlgorithmException; |
13 | 13 | import java.security.PrivateKey; |
14 | 14 | import java.security.PublicKey; |
| 15 | +import java.security.interfaces.RSAPublicKey; |
| 16 | +import java.security.spec.InvalidKeySpecException; |
| 17 | +import java.security.spec.PKCS8EncodedKeySpec; |
15 | 18 | import java.util.Objects; |
16 | 19 | import javax.crypto.BadPaddingException; |
17 | 20 | import javax.crypto.Cipher; |
|
21 | 24 |
|
22 | 25 | /** |
23 | 26 | * Given a public/private key pair, this class uses RSA to encrypt/decrypt data. |
| 27 | + * |
| 28 | + * <p>Keys are stored in standard formats: |
| 29 | + * <ul> |
| 30 | + * <li>Private key: PKCS#8 PEM ({@code -----BEGIN PRIVATE KEY-----})</li> |
| 31 | + * <li>Public key: OpenSSH format ({@code ssh-rsa <base64> <label>})</li> |
| 32 | + * </ul> |
| 33 | + * These formats are interoperable with OpenSSH, Node.js crypto, and other standard tools. |
24 | 34 | */ |
25 | 35 | public class RSAEncrypt { |
26 | 36 |
|
27 | | - /** |
28 | | - * The RSA algorithm key. |
29 | | - */ |
30 | 37 | private static final String ALGORITHM = "RSA"; |
| 38 | + private static final int KEY_SIZE = 2048; |
31 | 39 |
|
32 | 40 | /** |
33 | | - * Generates a new key, and stores the value in the RSA |
| 41 | + * Generates a new RSA key pair. |
34 | 42 | * |
35 | 43 | * @param label The label that will be associated with the public key |
36 | | - * @return |
| 44 | + * (e.g. "user@host") |
| 45 | + * @return A new RSAEncrypt instance with both keys |
37 | 46 | */ |
38 | 47 | public static RSAEncrypt generateKey(String label) { |
39 | 48 | KeyPairGenerator keyGen; |
40 | 49 | try { |
41 | 50 | keyGen = KeyPairGenerator.getInstance(ALGORITHM); |
42 | | - } catch (NoSuchAlgorithmException ex) { |
| 51 | + } catch(NoSuchAlgorithmException ex) { |
43 | 52 | throw new RuntimeException(ex); |
44 | 53 | } |
45 | | - keyGen.initialize(1024); |
| 54 | + keyGen.initialize(KEY_SIZE); |
46 | 55 | KeyPair key = keyGen.generateKeyPair(); |
47 | | - RSAEncrypt enc = new RSAEncrypt(toString(key.getPrivate()), toString(key.getPublic(), label)); |
| 56 | + RSAEncrypt enc = new RSAEncrypt( |
| 57 | + privateKeyToPem(key.getPrivate()), |
| 58 | + publicKeyToSsh(key.getPublic(), label)); |
48 | 59 | return enc; |
49 | 60 | } |
50 | 61 |
|
51 | 62 | /** |
52 | | - * Given a public key and a label, produces an ssh compatible rsa public key string. |
53 | | - * |
54 | | - * @param key |
55 | | - * @param label |
56 | | - * @return |
| 63 | + * Encodes a private key as a PKCS#8 PEM string. |
| 64 | + */ |
| 65 | + private static String privateKeyToPem(PrivateKey key) { |
| 66 | + String base64 = Base64.encodeBase64String(key.getEncoded()); |
| 67 | + StringBuilder sb = new StringBuilder(); |
| 68 | + sb.append("-----BEGIN PRIVATE KEY-----"); |
| 69 | + for(int i = 0; i < base64.length(); i++) { |
| 70 | + if(i % 64 == 0) { |
| 71 | + sb.append(StringUtils.nl()); |
| 72 | + } |
| 73 | + sb.append(base64.charAt(i)); |
| 74 | + } |
| 75 | + sb.append(StringUtils.nl()).append("-----END PRIVATE KEY-----").append(StringUtils.nl()); |
| 76 | + return sb.toString(); |
| 77 | + } |
| 78 | + |
| 79 | + /** |
| 80 | + * Encodes a public key in OpenSSH format: {@code ssh-rsa <base64> <label>}. |
| 81 | + * The base64 payload uses the SSH wire format (RFC 4253): |
| 82 | + * string "ssh-rsa", mpint e, mpint n. |
57 | 83 | */ |
58 | | - public static String toString(PublicKey key, String label) { |
| 84 | + private static String publicKeyToSsh(PublicKey key, String label) { |
59 | 85 | Objects.requireNonNull(label); |
60 | | - ByteArrayOutputStream pubBOS = new ByteArrayOutputStream(); |
| 86 | + RSAPublicKey rsaKey = (RSAPublicKey) key; |
61 | 87 | try { |
62 | | - ObjectOutputStream publicKeyOS = new ObjectOutputStream(pubBOS); |
63 | | - publicKeyOS.writeObject(key); |
64 | | - } catch (IOException ex) { |
| 88 | + ByteArrayOutputStream buf = new ByteArrayOutputStream(); |
| 89 | + DataOutputStream dos = new DataOutputStream(buf); |
| 90 | + byte[] typeBytes = "ssh-rsa".getBytes("UTF-8"); |
| 91 | + dos.writeInt(typeBytes.length); |
| 92 | + dos.write(typeBytes); |
| 93 | + byte[] eBytes = rsaKey.getPublicExponent().toByteArray(); |
| 94 | + dos.writeInt(eBytes.length); |
| 95 | + dos.write(eBytes); |
| 96 | + byte[] nBytes = rsaKey.getModulus().toByteArray(); |
| 97 | + dos.writeInt(nBytes.length); |
| 98 | + dos.write(nBytes); |
| 99 | + dos.flush(); |
| 100 | + return "ssh-rsa " + Base64.encodeBase64String(buf.toByteArray()) + " " + label; |
| 101 | + } catch(IOException ex) { |
65 | 102 | throw new RuntimeException(ex); |
66 | 103 | } |
67 | | - String publicKey = Base64.encodeBase64String(pubBOS.toByteArray()); |
68 | | - publicKey = "ssh-rsa " + publicKey + " " + label; |
69 | | - return publicKey; |
70 | 104 | } |
71 | 105 |
|
72 | 106 | /** |
73 | | - * Given a private key, produces an ssh compatible rsa private key string. |
74 | | - * |
75 | | - * @param key |
76 | | - * @return |
| 107 | + * Parses an OpenSSH public key string and returns the PublicKey. |
| 108 | + * Reads the SSH wire format: string "ssh-rsa", mpint e, mpint n. |
77 | 109 | */ |
78 | | - private static String toString(PrivateKey key) { |
79 | | - ByteArrayOutputStream privBOS = new ByteArrayOutputStream(); |
80 | | - ObjectOutputStream privateKeyOS; |
| 110 | + private static PublicKey sshToPublicKey(String base64Part) { |
| 111 | + byte[] decoded = Base64.decodeBase64(base64Part); |
| 112 | + ByteBuffer bb = ByteBuffer.wrap(decoded); |
| 113 | + // Read key type string |
| 114 | + int typeLen = bb.getInt(); |
| 115 | + byte[] typeBytes = new byte[typeLen]; |
| 116 | + bb.get(typeBytes); |
| 117 | + // Read exponent |
| 118 | + int eLen = bb.getInt(); |
| 119 | + byte[] eBytes = new byte[eLen]; |
| 120 | + bb.get(eBytes); |
| 121 | + java.math.BigInteger e = new java.math.BigInteger(eBytes); |
| 122 | + // Read modulus |
| 123 | + int nLen = bb.getInt(); |
| 124 | + byte[] nBytes = new byte[nLen]; |
| 125 | + bb.get(nBytes); |
| 126 | + java.math.BigInteger n = new java.math.BigInteger(nBytes); |
81 | 127 | try { |
82 | | - privateKeyOS = new ObjectOutputStream(privBOS); |
83 | | - privateKeyOS.writeObject(key); |
84 | | - } catch (IOException ex) { |
| 128 | + java.security.spec.RSAPublicKeySpec spec = new java.security.spec.RSAPublicKeySpec(n, e); |
| 129 | + KeyFactory kf = KeyFactory.getInstance(ALGORITHM); |
| 130 | + return kf.generatePublic(spec); |
| 131 | + } catch(NoSuchAlgorithmException | InvalidKeySpecException ex) { |
85 | 132 | throw new RuntimeException(ex); |
86 | 133 | } |
87 | | - String privateKey = Base64.encodeBase64String(privBOS.toByteArray()); |
88 | | - |
89 | | - StringBuilder privBuilder = new StringBuilder(); |
90 | | - privBuilder.append("-----BEGIN RSA PRIVATE KEY-----"); |
91 | | - for(int i = 0; i < privateKey.length(); i++) { |
92 | | - if(i % 64 == 0) { |
93 | | - privBuilder.append(StringUtils.nl()); |
94 | | - } |
95 | | - privBuilder.append(privateKey.charAt(i)); |
96 | | - } |
97 | | - privBuilder.append(StringUtils.nl()).append("-----END RSA PRIVATE KEY-----").append(StringUtils.nl()); |
98 | | - privateKey = privBuilder.toString(); |
99 | | - return privateKey; |
100 | 134 | } |
101 | 135 |
|
102 | 136 | private PublicKey publicKey; |
103 | 137 | private PrivateKey privateKey; |
104 | 138 | private String label; |
105 | 139 |
|
106 | 140 | /** |
107 | | - * Creates a new RSAEncrypt object, based on the ssh compatible private/public key pair. Only one key needs to be |
108 | | - * provided. If so, only those methods for the key provided will work. |
| 141 | + * Creates a new RSAEncrypt object from PEM/SSH key strings. Only one key needs to be |
| 142 | + * provided. If so, only the methods for that key will work. |
| 143 | + * |
| 144 | + * <p>The private key should be PKCS#8 PEM format ({@code -----BEGIN PRIVATE KEY-----}). |
| 145 | + * The public key should be OpenSSH format ({@code ssh-rsa <base64> <label>}). |
109 | 146 | * |
110 | | - * @param privateKey |
111 | | - * @param publicKey |
112 | | - * @throws IllegalArgumentException If the keys are not the correct type. They must be ssh compatible. |
| 147 | + * @param privateKey The private key PEM string, or null |
| 148 | + * @param publicKey The public key SSH string, or null |
| 149 | + * @throws IllegalArgumentException If a key string cannot be parsed |
113 | 150 | */ |
114 | 151 | public RSAEncrypt(String privateKey, String publicKey) throws IllegalArgumentException { |
115 | 152 | if(privateKey != null) { |
116 | | - //private key processing |
117 | | - //replace all newlines with nothing |
118 | 153 | privateKey = privateKey.replaceAll("\r", ""); |
119 | 154 | privateKey = privateKey.replaceAll("\n", ""); |
120 | | - //Remove the BEGIN/END tags |
| 155 | + privateKey = privateKey.replace("-----BEGIN PRIVATE KEY-----", ""); |
| 156 | + privateKey = privateKey.replace("-----END PRIVATE KEY-----", ""); |
| 157 | + // Also strip PKCS#1 headers for compatibility with ssh-keygen keys |
121 | 158 | privateKey = privateKey.replace("-----BEGIN RSA PRIVATE KEY-----", ""); |
122 | 159 | privateKey = privateKey.replace("-----END RSA PRIVATE KEY-----", ""); |
123 | | - ObjectInputStream privOIS; |
124 | 160 | try { |
125 | | - privOIS = new ObjectInputStream(new ByteArrayInputStream(Base64.decodeBase64(privateKey))); |
126 | | - this.privateKey = (PrivateKey) privOIS.readObject(); |
127 | | - } catch (IOException | ClassNotFoundException ex) { |
128 | | - throw new RuntimeException(ex); |
| 161 | + byte[] keyBytes = Base64.decodeBase64(privateKey); |
| 162 | + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); |
| 163 | + KeyFactory kf = KeyFactory.getInstance(ALGORITHM); |
| 164 | + this.privateKey = kf.generatePrivate(spec); |
| 165 | + } catch(NoSuchAlgorithmException | InvalidKeySpecException ex) { |
| 166 | + throw new IllegalArgumentException("Failed to parse private key", ex); |
129 | 167 | } |
130 | 168 | } |
131 | 169 |
|
132 | 170 | if(publicKey != null) { |
133 | | - //public key processing |
134 | | - String[] split = publicKey.split(" "); |
135 | | - if(split.length != 3) { |
136 | | - throw new IllegalArgumentException("Invalid public key passed in."); |
| 171 | + String[] split = publicKey.trim().split("\\s+"); |
| 172 | + if(split.length < 2) { |
| 173 | + throw new IllegalArgumentException("Invalid public key format."); |
137 | 174 | } |
138 | 175 | if(!"ssh-rsa".equals(split[0])) { |
139 | | - throw new IllegalArgumentException("Invalid public key type. Expecting ssh-rsa, but found \"" + split[0] + "\""); |
140 | | - } |
141 | | - this.label = split[2]; |
142 | | - ObjectInputStream pubOIS; |
143 | | - try { |
144 | | - pubOIS = new ObjectInputStream(new ByteArrayInputStream(Base64.decodeBase64(split[1]))); |
145 | | - this.publicKey = (PublicKey) pubOIS.readObject(); |
146 | | - } catch (IOException | ClassNotFoundException ex) { |
147 | | - throw new RuntimeException(ex); |
| 176 | + throw new IllegalArgumentException( |
| 177 | + "Invalid public key type. Expecting ssh-rsa, but found \"" |
| 178 | + + split[0] + "\""); |
148 | 179 | } |
| 180 | + this.label = split.length >= 3 ? split[2] : ""; |
| 181 | + this.publicKey = sshToPublicKey(split[1]); |
149 | 182 | } |
150 | 183 | } |
151 | 184 |
|
152 | 185 | /** |
153 | | - * Encrypts the data with the public key, which can be decrypted with the private key. This is only valid if the |
154 | | - * public key was provided. |
155 | | - * |
156 | | - * @param data |
157 | | - * @return |
| 186 | + * Encrypts the data with the public key, which can be decrypted with the private key. |
158 | 187 | */ |
159 | 188 | public byte[] encryptWithPublic(byte[] data) { |
160 | 189 | Objects.requireNonNull(publicKey); |
161 | 190 | return crypt(data, publicKey, Cipher.ENCRYPT_MODE); |
162 | 191 | } |
163 | 192 |
|
164 | 193 | /** |
165 | | - * Encrypts the data with the private key, which can be decrypted with the public key. This is only valid if the |
166 | | - * private key was provided. |
167 | | - * |
168 | | - * @param data |
169 | | - * @return |
170 | | - * @throws InvalidKeyException |
| 194 | + * Encrypts the data with the private key, which can be decrypted with the public key. |
171 | 195 | */ |
172 | 196 | public byte[] encryptWithPrivate(byte[] data) throws InvalidKeyException { |
173 | 197 | Objects.requireNonNull(privateKey); |
174 | 198 | return crypt(data, privateKey, Cipher.ENCRYPT_MODE); |
175 | 199 | } |
176 | 200 |
|
177 | 201 | /** |
178 | | - * Decrypts the data with the public key, which will have been encrypted with the private key. This is only valid if |
179 | | - * the public key was provided. |
180 | | - * |
181 | | - * @param data |
182 | | - * @return |
| 202 | + * Decrypts the data with the public key, which will have been encrypted with the private key. |
183 | 203 | */ |
184 | 204 | public byte[] decryptWithPublic(byte[] data) { |
185 | 205 | Objects.requireNonNull(publicKey); |
186 | 206 | return crypt(data, publicKey, Cipher.DECRYPT_MODE); |
187 | 207 | } |
188 | 208 |
|
189 | 209 | /** |
190 | | - * Decrypts the data with the private key, which will have been encrypted with the public key. This is only valid if |
191 | | - * the private key was provided. |
192 | | - * |
193 | | - * @param data |
194 | | - * @return |
| 210 | + * Decrypts the data with the private key, which will have been encrypted with the public key. |
195 | 211 | */ |
196 | 212 | public byte[] decryptWithPrivate(byte[] data) { |
197 | 213 | Objects.requireNonNull(privateKey); |
198 | 214 | return crypt(data, privateKey, Cipher.DECRYPT_MODE); |
199 | 215 | } |
200 | 216 |
|
201 | | - /** |
202 | | - * Utility method that actually does the de/encrypting. |
203 | | - * |
204 | | - * @param data |
205 | | - * @param key |
206 | | - * @param cryptMode |
207 | | - * @return |
208 | | - */ |
209 | 217 | private byte[] crypt(byte[] data, Key key, int cryptMode) { |
210 | | - byte[] cipherValue = null; |
211 | | - Cipher cipher; |
212 | 218 | try { |
213 | | - cipher = Cipher.getInstance(ALGORITHM); |
| 219 | + Cipher cipher = Cipher.getInstance(ALGORITHM); |
214 | 220 | cipher.init(cryptMode, key); |
215 | | - cipherValue = cipher.doFinal(data); |
216 | | - } catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException ex) { |
| 221 | + return cipher.doFinal(data); |
| 222 | + } catch(InvalidKeyException | IllegalBlockSizeException | BadPaddingException |
| 223 | + | NoSuchAlgorithmException | NoSuchPaddingException ex) { |
217 | 224 | throw new RuntimeException(ex); |
218 | 225 | } |
219 | | - return cipherValue; |
220 | 226 | } |
221 | 227 |
|
222 | 228 | /** |
223 | | - * Returns the private key string. |
224 | | - * |
225 | | - * @return |
| 229 | + * Returns the private key as a PKCS#8 PEM string. |
226 | 230 | */ |
227 | 231 | public String getPrivateKey() { |
228 | | - return toString(privateKey); |
| 232 | + return privateKeyToPem(privateKey); |
229 | 233 | } |
230 | 234 |
|
231 | 235 | /** |
232 | | - * Returns the public key string. |
233 | | - * |
234 | | - * @return |
| 236 | + * Returns the public key as an OpenSSH format string. |
235 | 237 | */ |
236 | 238 | public String getPublicKey() { |
237 | | - return toString(publicKey, label); |
| 239 | + return publicKeyToSsh(publicKey, label); |
238 | 240 | } |
239 | 241 |
|
240 | 242 | /** |
241 | 243 | * Returns the label on the public key. |
242 | | - * |
243 | | - * @return |
244 | 244 | */ |
245 | 245 | public String getLabel() { |
246 | 246 | return label; |
|
0 commit comments