Skip to content

Commit 8398512

Browse files
committed
Add attach mode and KEYPAIR security
1 parent 533179a commit 8398512

20 files changed

Lines changed: 1441 additions & 299 deletions

pom.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@
133133
<properties>
134134
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
135135
<maven.test.failure.ignore>true</maven.test.failure.ignore>
136-
<maven.compiler.source>16</maven.compiler.source>
137-
<maven.compiler.target>16</maven.compiler.target>
136+
<maven.compiler.source>21</maven.compiler.source>
137+
<maven.compiler.target>21</maven.compiler.target>
138138
</properties>
139139
<repositories>
140140
<repository>
@@ -494,7 +494,7 @@
494494
<version>3.12.1</version>
495495
<configuration>
496496
<showDeprecation>true</showDeprecation>
497-
<release>16</release>
497+
<release>21</release>
498498
<compilerArgs>
499499
<arg>-XDignore.symbol.file</arg>
500500
<arg>-parameters</arg>

src/main/java/com/laytonsmith/PureUtilities/Common/RSAEncrypt.java

Lines changed: 118 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
package com.laytonsmith.PureUtilities.Common;
22

3-
import java.io.ByteArrayInputStream;
43
import java.io.ByteArrayOutputStream;
4+
import java.io.DataOutputStream;
55
import java.io.IOException;
6-
import java.io.ObjectInputStream;
7-
import java.io.ObjectOutputStream;
6+
import java.nio.ByteBuffer;
87
import java.security.InvalidKeyException;
98
import java.security.Key;
9+
import java.security.KeyFactory;
1010
import java.security.KeyPair;
1111
import java.security.KeyPairGenerator;
1212
import java.security.NoSuchAlgorithmException;
1313
import java.security.PrivateKey;
1414
import java.security.PublicKey;
15+
import java.security.interfaces.RSAPublicKey;
16+
import java.security.spec.InvalidKeySpecException;
17+
import java.security.spec.PKCS8EncodedKeySpec;
1518
import java.util.Objects;
1619
import javax.crypto.BadPaddingException;
1720
import javax.crypto.Cipher;
@@ -21,226 +24,223 @@
2124

2225
/**
2326
* 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.
2434
*/
2535
public class RSAEncrypt {
2636

27-
/**
28-
* The RSA algorithm key.
29-
*/
3037
private static final String ALGORITHM = "RSA";
38+
private static final int KEY_SIZE = 2048;
3139

3240
/**
33-
* Generates a new key, and stores the value in the RSA
41+
* Generates a new RSA key pair.
3442
*
3543
* @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
3746
*/
3847
public static RSAEncrypt generateKey(String label) {
3948
KeyPairGenerator keyGen;
4049
try {
4150
keyGen = KeyPairGenerator.getInstance(ALGORITHM);
42-
} catch (NoSuchAlgorithmException ex) {
51+
} catch(NoSuchAlgorithmException ex) {
4352
throw new RuntimeException(ex);
4453
}
45-
keyGen.initialize(1024);
54+
keyGen.initialize(KEY_SIZE);
4655
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));
4859
return enc;
4960
}
5061

5162
/**
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.
5783
*/
58-
public static String toString(PublicKey key, String label) {
84+
private static String publicKeyToSsh(PublicKey key, String label) {
5985
Objects.requireNonNull(label);
60-
ByteArrayOutputStream pubBOS = new ByteArrayOutputStream();
86+
RSAPublicKey rsaKey = (RSAPublicKey) key;
6187
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) {
65102
throw new RuntimeException(ex);
66103
}
67-
String publicKey = Base64.encodeBase64String(pubBOS.toByteArray());
68-
publicKey = "ssh-rsa " + publicKey + " " + label;
69-
return publicKey;
70104
}
71105

72106
/**
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.
77109
*/
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);
81127
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) {
85132
throw new RuntimeException(ex);
86133
}
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;
100134
}
101135

102136
private PublicKey publicKey;
103137
private PrivateKey privateKey;
104138
private String label;
105139

106140
/**
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>}).
109146
*
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
113150
*/
114151
public RSAEncrypt(String privateKey, String publicKey) throws IllegalArgumentException {
115152
if(privateKey != null) {
116-
//private key processing
117-
//replace all newlines with nothing
118153
privateKey = privateKey.replaceAll("\r", "");
119154
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
121158
privateKey = privateKey.replace("-----BEGIN RSA PRIVATE KEY-----", "");
122159
privateKey = privateKey.replace("-----END RSA PRIVATE KEY-----", "");
123-
ObjectInputStream privOIS;
124160
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);
129167
}
130168
}
131169

132170
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.");
137174
}
138175
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] + "\"");
148179
}
180+
this.label = split.length >= 3 ? split[2] : "";
181+
this.publicKey = sshToPublicKey(split[1]);
149182
}
150183
}
151184

152185
/**
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.
158187
*/
159188
public byte[] encryptWithPublic(byte[] data) {
160189
Objects.requireNonNull(publicKey);
161190
return crypt(data, publicKey, Cipher.ENCRYPT_MODE);
162191
}
163192

164193
/**
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.
171195
*/
172196
public byte[] encryptWithPrivate(byte[] data) throws InvalidKeyException {
173197
Objects.requireNonNull(privateKey);
174198
return crypt(data, privateKey, Cipher.ENCRYPT_MODE);
175199
}
176200

177201
/**
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.
183203
*/
184204
public byte[] decryptWithPublic(byte[] data) {
185205
Objects.requireNonNull(publicKey);
186206
return crypt(data, publicKey, Cipher.DECRYPT_MODE);
187207
}
188208

189209
/**
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.
195211
*/
196212
public byte[] decryptWithPrivate(byte[] data) {
197213
Objects.requireNonNull(privateKey);
198214
return crypt(data, privateKey, Cipher.DECRYPT_MODE);
199215
}
200216

201-
/**
202-
* Utility method that actually does the de/encrypting.
203-
*
204-
* @param data
205-
* @param key
206-
* @param cryptMode
207-
* @return
208-
*/
209217
private byte[] crypt(byte[] data, Key key, int cryptMode) {
210-
byte[] cipherValue = null;
211-
Cipher cipher;
212218
try {
213-
cipher = Cipher.getInstance(ALGORITHM);
219+
Cipher cipher = Cipher.getInstance(ALGORITHM);
214220
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) {
217224
throw new RuntimeException(ex);
218225
}
219-
return cipherValue;
220226
}
221227

222228
/**
223-
* Returns the private key string.
224-
*
225-
* @return
229+
* Returns the private key as a PKCS#8 PEM string.
226230
*/
227231
public String getPrivateKey() {
228-
return toString(privateKey);
232+
return privateKeyToPem(privateKey);
229233
}
230234

231235
/**
232-
* Returns the public key string.
233-
*
234-
* @return
236+
* Returns the public key as an OpenSSH format string.
235237
*/
236238
public String getPublicKey() {
237-
return toString(publicKey, label);
239+
return publicKeyToSsh(publicKey, label);
238240
}
239241

240242
/**
241243
* Returns the label on the public key.
242-
*
243-
* @return
244244
*/
245245
public String getLabel() {
246246
return label;

src/main/java/com/laytonsmith/commandhelper/CommandHelperFileLocations.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,13 @@ public File getUpgradeLogFile() {
3131
return new File(getCacheDirectory(), "upgradeLog.json");
3232
}
3333

34+
/**
35+
* Returns the location of the authorized_debug_keys file.
36+
* @return
37+
*/
38+
@Override
39+
public File getAuthorizedDebugKeysFile() {
40+
return new File(getConfigDirectory(), "authorized_debug_keys");
41+
}
42+
3443
}

0 commit comments

Comments
 (0)