diff --git a/src/main/java/com/laytonsmith/PureUtilities/Common/RSAEncrypt.java b/src/main/java/com/laytonsmith/PureUtilities/Common/RSAEncrypt.java deleted file mode 100644 index c9f84e2b80..0000000000 --- a/src/main/java/com/laytonsmith/PureUtilities/Common/RSAEncrypt.java +++ /dev/null @@ -1,249 +0,0 @@ -package com.laytonsmith.PureUtilities.Common; - -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.Objects; -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import org.apache.commons.codec.binary.Base64; - -/** - * Given a public/private key pair, this class uses RSA to encrypt/decrypt data. - * - *
Keys are stored in standard formats: - *
The private key should be PKCS#8 PEM format ({@code -----BEGIN PRIVATE KEY-----}). - * The public key should be OpenSSH format ({@code ssh-rsa }). - * - * @param privateKey The private key PEM string, or null - * @param publicKey The public key SSH string, or null - * @throws IllegalArgumentException If a key string cannot be parsed - */ - public RSAEncrypt(String privateKey, String publicKey) throws IllegalArgumentException { - if(privateKey != null) { - privateKey = privateKey.replaceAll("\r", ""); - privateKey = privateKey.replaceAll("\n", ""); - privateKey = privateKey.replace("-----BEGIN PRIVATE KEY-----", ""); - privateKey = privateKey.replace("-----END PRIVATE KEY-----", ""); - // Also strip PKCS#1 headers for compatibility with ssh-keygen keys - privateKey = privateKey.replace("-----BEGIN RSA PRIVATE KEY-----", ""); - privateKey = privateKey.replace("-----END RSA PRIVATE KEY-----", ""); - try { - byte[] keyBytes = Base64.decodeBase64(privateKey); - PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); - KeyFactory kf = KeyFactory.getInstance(ALGORITHM); - this.privateKey = kf.generatePrivate(spec); - } catch(NoSuchAlgorithmException | InvalidKeySpecException ex) { - throw new IllegalArgumentException("Failed to parse private key", ex); - } - } - - if(publicKey != null) { - String[] split = publicKey.trim().split("\\s+"); - if(split.length < 2) { - throw new IllegalArgumentException("Invalid public key format."); - } - if(!"ssh-rsa".equals(split[0])) { - throw new IllegalArgumentException( - "Invalid public key type. Expecting ssh-rsa, but found \"" - + split[0] + "\""); - } - this.label = split.length >= 3 ? split[2] : ""; - this.publicKey = sshToPublicKey(split[1]); - } - } - - /** - * Encrypts the data with the public key, which can be decrypted with the private key. - */ - public byte[] encryptWithPublic(byte[] data) { - Objects.requireNonNull(publicKey); - return crypt(data, publicKey, Cipher.ENCRYPT_MODE); - } - - /** - * Encrypts the data with the private key, which can be decrypted with the public key. - */ - public byte[] encryptWithPrivate(byte[] data) throws InvalidKeyException { - Objects.requireNonNull(privateKey); - return crypt(data, privateKey, Cipher.ENCRYPT_MODE); - } - - /** - * Decrypts the data with the public key, which will have been encrypted with the private key. - */ - public byte[] decryptWithPublic(byte[] data) { - Objects.requireNonNull(publicKey); - return crypt(data, publicKey, Cipher.DECRYPT_MODE); - } - - /** - * Decrypts the data with the private key, which will have been encrypted with the public key. - */ - public byte[] decryptWithPrivate(byte[] data) { - Objects.requireNonNull(privateKey); - return crypt(data, privateKey, Cipher.DECRYPT_MODE); - } - - private byte[] crypt(byte[] data, Key key, int cryptMode) { - try { - Cipher cipher = Cipher.getInstance(ALGORITHM); - cipher.init(cryptMode, key); - return cipher.doFinal(data); - } catch(InvalidKeyException | IllegalBlockSizeException | BadPaddingException - | NoSuchAlgorithmException | NoSuchPaddingException ex) { - throw new RuntimeException(ex); - } - } - - /** - * Returns the private key as a PKCS#8 PEM string. - */ - public String getPrivateKey() { - return privateKeyToPem(privateKey); - } - - /** - * Returns the public key as an OpenSSH format string. - */ - public String getPublicKey() { - return publicKeyToSsh(publicKey, label); - } - - /** - * Returns the label on the public key. - */ - public String getLabel() { - return label; - } - -} diff --git a/src/main/java/com/laytonsmith/PureUtilities/Common/SSHKeyPair.java b/src/main/java/com/laytonsmith/PureUtilities/Common/SSHKeyPair.java new file mode 100644 index 0000000000..3a47c81664 --- /dev/null +++ b/src/main/java/com/laytonsmith/PureUtilities/Common/SSHKeyPair.java @@ -0,0 +1,466 @@ +package com.laytonsmith.PureUtilities.Common; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.NamedParameterSpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Objects; +import org.apache.commons.codec.binary.Base64; + +/** + * Handles SSH key pair operations for multiple key types: RSA, Ed25519, and ECDSA. + * + * Keys are stored in standard formats: + * + * Private key: PKCS#8 PEM ({@code -----BEGIN PRIVATE KEY-----}) + * Public key: OpenSSH format ({@code ssh-rsa/ssh-ed25519/ecdsa-sha2-nistp256 }) + * + * These formats are interoperable with OpenSSH, Node.js crypto, and other standard tools. + */ +public class SSHKeyPair { + + /** + * Supported SSH key types. + */ + public enum KeyType { + RSA("ssh-rsa", "RSA", "SHA256withRSA", 2048), + ED25519("ssh-ed25519", "Ed25519", "Ed25519", 0), + ECDSA_256("ecdsa-sha2-nistp256", "EC", "SHA256withECDSA", 256); + + private final String sshName; + private final String jcaAlgorithm; + private final String signatureAlgorithm; + private final int keySize; + + KeyType(String sshName, String jcaAlgorithm, String signatureAlgorithm, int keySize) { + this.sshName = sshName; + this.jcaAlgorithm = jcaAlgorithm; + this.signatureAlgorithm = signatureAlgorithm; + this.keySize = keySize; + } + + public String getSshName() { + return sshName; + } + + public String getJcaAlgorithm() { + return jcaAlgorithm; + } + + public String getSignatureAlgorithm() { + return signatureAlgorithm; + } + + /** + * Returns the KeyType for the given SSH key type name. + * @param sshName The SSH key type name (e.g. "ssh-rsa", "ssh-ed25519") + * @return The matching KeyType + * @throws IllegalArgumentException If the key type is not supported + */ + public static KeyType fromSshName(String sshName) { + for(KeyType type : values()) { + if(type.sshName.equals(sshName)) { + return type; + } + } + throw new IllegalArgumentException("Unsupported SSH key type: " + sshName); + } + } + + private PublicKey publicKey; + private PrivateKey privateKey; + private KeyType keyType; + private String label; + + /** + * Generates a new key pair of the given type. + * + * @param type The key type to generate + * @param label The label for the public key (e.g. "user@host") + * @return A new SSHKeyPair instance with both keys + */ + public static SSHKeyPair generateKey(KeyType type, String label) { + try { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance(type.jcaAlgorithm); + switch(type) { + case RSA: + keyGen.initialize(type.keySize); + break; + case ED25519: + keyGen.initialize(new NamedParameterSpec("Ed25519")); + break; + case ECDSA_256: + keyGen.initialize(new ECGenParameterSpec("secp256r1")); + break; + default: + throw new UnsupportedOperationException("Unknown key type: " + type); + } + KeyPair pair = keyGen.generateKeyPair(); + SSHKeyPair result = new SSHKeyPair(); + result.keyType = type; + result.privateKey = pair.getPrivate(); + result.publicKey = pair.getPublic(); + result.label = label; + return result; + } catch(NoSuchAlgorithmException | java.security.InvalidAlgorithmParameterException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Creates an SSHKeyPair from PEM/SSH key strings. Either key may be null, + * but at least one must be provided. The key type is auto-detected from + * the key data. + * + * The private key should be PKCS#8 PEM format ({@code -----BEGIN PRIVATE KEY-----}). + * The public key should be OpenSSH format ({@code type base64 label}). + * + * @param privateKeyPem The private key PEM string, or null + * @param publicKeySsh The public key OpenSSH string, or null + * @throws IllegalArgumentException If a key string cannot be parsed + */ + public SSHKeyPair(String privateKeyPem, String publicKeySsh) throws IllegalArgumentException { + if(publicKeySsh != null) { + String[] split = publicKeySsh.trim().split("\\s+"); + if(split.length < 2) { + throw new IllegalArgumentException("Invalid public key format."); + } + this.keyType = KeyType.fromSshName(split[0]); + this.label = split.length >= 3 ? split[2] : ""; + this.publicKey = sshToPublicKey(keyType, split[1]); + } + + if(privateKeyPem != null) { + this.privateKey = parsePrivateKey(privateKeyPem); + if(this.keyType == null) { + this.keyType = detectKeyType(privateKey); + } + } + } + + private SSHKeyPair() { + // Used by generateKey + } + + /** + * Signs the given data with the private key. + * + * @param data The data to sign + * @return The signature bytes + */ + public byte[] sign(byte[] data) { + Objects.requireNonNull(privateKey, "Private key is required for signing"); + try { + Signature sig = Signature.getInstance(keyType.signatureAlgorithm); + sig.initSign(privateKey); + sig.update(data); + return sig.sign(); + } catch(NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Verifies a signature against the given data using the public key. + * + * @param data The original data that was signed + * @param signature The signature to verify + * @return true if the signature is valid + */ + public boolean verify(byte[] data, byte[] signature) { + Objects.requireNonNull(publicKey, "Public key is required for verification"); + try { + Signature sig = Signature.getInstance(keyType.signatureAlgorithm); + sig.initVerify(publicKey); + sig.update(data); + return sig.verify(signature); + } catch(NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Returns the private key as a PKCS#8 PEM string. + */ + public String getPrivateKeyPem() { + Objects.requireNonNull(privateKey); + String base64 = Base64.encodeBase64String(privateKey.getEncoded()); + StringBuilder sb = new StringBuilder(); + sb.append("-----BEGIN PRIVATE KEY-----"); + for(int i = 0; i < base64.length(); i++) { + if(i % 64 == 0) { + sb.append(StringUtils.nl()); + } + sb.append(base64.charAt(i)); + } + sb.append(StringUtils.nl()).append("-----END PRIVATE KEY-----").append(StringUtils.nl()); + return sb.toString(); + } + + /** + * Returns the public key as an OpenSSH format string. + */ + public String getPublicKeySsh() { + Objects.requireNonNull(publicKey); + return publicKeyToSsh(keyType, publicKey, label); + } + + /** + * Returns the key type. + */ + public KeyType getKeyType() { + return keyType; + } + + /** + * Returns the label on the public key. + */ + public String getLabel() { + return label; + } + + // --- Private key parsing --- + + private static PrivateKey parsePrivateKey(String pem) { + pem = pem.replaceAll("\r", "").replaceAll("\n", ""); + // Strip all known PEM headers + pem = pem.replace("-----BEGIN PRIVATE KEY-----", ""); + pem = pem.replace("-----END PRIVATE KEY-----", ""); + pem = pem.replace("-----BEGIN RSA PRIVATE KEY-----", ""); + pem = pem.replace("-----END RSA PRIVATE KEY-----", ""); + pem = pem.replace("-----BEGIN EC PRIVATE KEY-----", ""); + pem = pem.replace("-----END EC PRIVATE KEY-----", ""); + pem = pem.trim(); + byte[] keyBytes = Base64.decodeBase64(pem); + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); + + // Try each algorithm until one works + String[] algorithms = {"Ed25519", "EC", "RSA"}; + for(String alg : algorithms) { + try { + KeyFactory kf = KeyFactory.getInstance(alg); + return kf.generatePrivate(spec); + } catch(NoSuchAlgorithmException | InvalidKeySpecException ex) { + // Try next + } + } + throw new IllegalArgumentException("Failed to parse private key. " + + "Supported types: RSA, Ed25519, ECDSA (PKCS#8 format)."); + } + + private static KeyType detectKeyType(PrivateKey key) { + return switch(key.getAlgorithm()) { + case "RSA" -> KeyType.RSA; + case "Ed25519", "EdDSA" -> KeyType.ED25519; + case "EC" -> KeyType.ECDSA_256; + default -> throw new IllegalArgumentException( + "Unsupported private key algorithm: " + key.getAlgorithm()); + }; + } + + // --- OpenSSH public key encoding/decoding --- + + private static String publicKeyToSsh(KeyType type, PublicKey key, String label) { + Objects.requireNonNull(label); + try { + byte[] wireBytes = encodePublicKeyWire(type, key); + return type.sshName + " " + Base64.encodeBase64String(wireBytes) + " " + label; + } catch(IOException ex) { + throw new RuntimeException(ex); + } + } + + private static byte[] encodePublicKeyWire(KeyType type, PublicKey key) throws IOException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(buf); + byte[] typeBytes = type.sshName.getBytes("UTF-8"); + dos.writeInt(typeBytes.length); + dos.write(typeBytes); + + switch(type) { + case RSA: { + RSAPublicKey rsaKey = (RSAPublicKey) key; + byte[] eBytes = rsaKey.getPublicExponent().toByteArray(); + dos.writeInt(eBytes.length); + dos.write(eBytes); + byte[] nBytes = rsaKey.getModulus().toByteArray(); + dos.writeInt(nBytes.length); + dos.write(nBytes); + break; + } + case ED25519: { + // Ed25519 public key is the raw 32-byte key from X.509 encoding + byte[] rawKey = extractEd25519RawPublicKey(key); + dos.writeInt(rawKey.length); + dos.write(rawKey); + break; + } + case ECDSA_256: { + ECPublicKey ecKey = (ECPublicKey) key; + // Write curve identifier + byte[] curveBytes = "nistp256".getBytes("UTF-8"); + dos.writeInt(curveBytes.length); + dos.write(curveBytes); + // Write uncompressed point (04 || x || y) + byte[] point = encodeEcPoint(ecKey); + dos.writeInt(point.length); + dos.write(point); + break; + } + default: + throw new UnsupportedOperationException("Unknown key type: " + type); + } + dos.flush(); + return buf.toByteArray(); + } + + private static PublicKey sshToPublicKey(KeyType type, String base64Part) { + byte[] decoded = Base64.decodeBase64(base64Part); + ByteBuffer bb = ByteBuffer.wrap(decoded); + // Skip key type string + int typeLen = bb.getInt(); + byte[] typeBytes = new byte[typeLen]; + bb.get(typeBytes); + + try { + return switch(type) { + case RSA -> decodeRsaPublicKey(bb); + case ED25519 -> decodeEd25519PublicKey(bb); + case ECDSA_256 -> decodeEcdsaPublicKey(bb); + }; + } catch(NoSuchAlgorithmException | InvalidKeySpecException ex) { + throw new RuntimeException("Failed to parse " + type.sshName + " public key", ex); + } + } + + private static PublicKey decodeRsaPublicKey(ByteBuffer bb) + throws NoSuchAlgorithmException, InvalidKeySpecException { + int eLen = bb.getInt(); + byte[] eBytes = new byte[eLen]; + bb.get(eBytes); + BigInteger e = new BigInteger(eBytes); + int nLen = bb.getInt(); + byte[] nBytes = new byte[nLen]; + bb.get(nBytes); + BigInteger n = new BigInteger(nBytes); + java.security.spec.RSAPublicKeySpec spec = new java.security.spec.RSAPublicKeySpec(n, e); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePublic(spec); + } + + private static PublicKey decodeEd25519PublicKey(ByteBuffer bb) + throws NoSuchAlgorithmException, InvalidKeySpecException { + int keyLen = bb.getInt(); + byte[] rawKey = new byte[keyLen]; + bb.get(rawKey); + // Wrap raw 32 bytes into X.509 SubjectPublicKeyInfo for Ed25519 + // The X.509 prefix for Ed25519 is fixed: 30 2a 30 05 06 03 2b 65 70 03 21 00 + byte[] x509Prefix = { + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00 + }; + byte[] x509Key = new byte[x509Prefix.length + rawKey.length]; + System.arraycopy(x509Prefix, 0, x509Key, 0, x509Prefix.length); + System.arraycopy(rawKey, 0, x509Key, x509Prefix.length, rawKey.length); + X509EncodedKeySpec spec = new X509EncodedKeySpec(x509Key); + KeyFactory kf = KeyFactory.getInstance("Ed25519"); + return kf.generatePublic(spec); + } + + private static PublicKey decodeEcdsaPublicKey(ByteBuffer bb) + throws NoSuchAlgorithmException, InvalidKeySpecException { + // Skip curve identifier string + int curveLen = bb.getInt(); + byte[] curveBytes = new byte[curveLen]; + bb.get(curveBytes); + // Read uncompressed EC point + int pointLen = bb.getInt(); + byte[] pointBytes = new byte[pointLen]; + bb.get(pointBytes); + java.security.spec.ECPoint point = decodeEcPoint(pointBytes); + java.security.spec.ECPublicKeySpec spec = new java.security.spec.ECPublicKeySpec( + point, getP256Params()); + KeyFactory kf = KeyFactory.getInstance("EC"); + return kf.generatePublic(spec); + } + + // --- EC point encoding/decoding helpers --- + + private static byte[] encodeEcPoint(ECPublicKey key) { + byte[] x = key.getW().getAffineX().toByteArray(); + byte[] y = key.getW().getAffineY().toByteArray(); + // Pad/trim to 32 bytes each for P-256 + x = padOrTrimTo(x, 32); + y = padOrTrimTo(y, 32); + byte[] result = new byte[1 + 32 + 32]; + result[0] = 0x04; // uncompressed + System.arraycopy(x, 0, result, 1, 32); + System.arraycopy(y, 0, result, 33, 32); + return result; + } + + private static java.security.spec.ECPoint decodeEcPoint(byte[] data) { + if(data[0] != 0x04) { + throw new IllegalArgumentException("Only uncompressed EC points are supported"); + } + int coordLen = (data.length - 1) / 2; + byte[] xBytes = new byte[coordLen]; + byte[] yBytes = new byte[coordLen]; + System.arraycopy(data, 1, xBytes, 0, coordLen); + System.arraycopy(data, 1 + coordLen, yBytes, 0, coordLen); + return new java.security.spec.ECPoint(new BigInteger(1, xBytes), new BigInteger(1, yBytes)); + } + + private static java.security.spec.ECParameterSpec getP256Params() { + try { + java.security.AlgorithmParameters params = + java.security.AlgorithmParameters.getInstance("EC"); + params.init(new ECGenParameterSpec("secp256r1")); + return params.getParameterSpec(java.security.spec.ECParameterSpec.class); + } catch(Exception ex) { + throw new RuntimeException(ex); + } + } + + private static byte[] padOrTrimTo(byte[] input, int length) { + if(input.length == length) { + return input; + } + byte[] result = new byte[length]; + if(input.length > length) { + // Trim leading bytes (BigInteger may have a leading zero for sign) + System.arraycopy(input, input.length - length, result, 0, length); + } else { + // Pad with leading zeros + System.arraycopy(input, 0, result, length - input.length, input.length); + } + return result; + } + + /** + * Extracts the raw 32-byte Ed25519 public key from the X.509 encoding. + */ + private static byte[] extractEd25519RawPublicKey(PublicKey key) { + byte[] x509 = key.getEncoded(); + // X.509 for Ed25519: 12-byte prefix + 32-byte raw key + byte[] raw = new byte[32]; + System.arraycopy(x509, x509.length - 32, raw, 0, 32); + return raw; + } +} diff --git a/src/main/java/com/laytonsmith/core/Main.java b/src/main/java/com/laytonsmith/core/Main.java index 74f74f162a..49ea66ffcf 100644 --- a/src/main/java/com/laytonsmith/core/Main.java +++ b/src/main/java/com/laytonsmith/core/Main.java @@ -9,7 +9,7 @@ import com.laytonsmith.PureUtilities.Common.ArrayUtils; import com.laytonsmith.PureUtilities.Common.FileUtil; import com.laytonsmith.PureUtilities.Common.OSUtils; -import com.laytonsmith.PureUtilities.Common.RSAEncrypt; +import com.laytonsmith.PureUtilities.Common.SSHKeyPair; import com.laytonsmith.PureUtilities.Common.StreamUtils; import com.laytonsmith.PureUtilities.Common.StringUtils; import com.laytonsmith.PureUtilities.Common.UIUtils; @@ -1524,27 +1524,41 @@ public void execute(ArgumentParser.ArgumentParserResults parsedArgs) throws Exce } @tool("key-gen") - public static class RSAKeyGenMode extends AbstractCommandLineTool { + public static class KeyGenMode extends AbstractCommandLineTool { @Override public ArgumentParser getArgumentParser() { return ArgumentParser.GetParser() - .addDescription("Creates an ssh compatible rsa key pair, suitable for use with the" + .addDescription("Creates an ssh compatible key pair, suitable for use with the" + " MethodScript debugger's KEYPAIR security mode, or other tools that" - + " require RSA key authentication.") + + " require key authentication. Supports " + + StringUtils.Join(SSHKeyPair.KeyType.values(), ", ", + ", or ", " or ", null, (kt) -> kt.name()) + + " key types.") .addArgument(new ArgumentBuilder() - .setDescription("Output file for the keys. For instance, \"/home/user/.ssh/id_rsa\"." + .setDescription("Output file for the keys. For instance," + + " \"/home/user/.ssh/id_ed25519\"." + " The public key will have the same name, with \".pub\" appended.") .setUsageName("file") .setRequired() .setName('o', "output-file") .setArgType(ArgumentBuilder.BuilderTypeNonFlag.STRING)) .addArgument(new ArgumentBuilder() - .setDescription("Label for the public key. For instance, \"user@localhost\" or an email" - + " address.") + .setDescription("Label for the public key. For instance," + + " \"user@localhost\" or an email address.") .setUsageName("label") .setRequired() .setName('l', "label") + .setArgType(ArgumentBuilder.BuilderTypeNonFlag.STRING)) + .addArgument(new ArgumentBuilder() + .setDescription("Key type to generate. Options: " + + StringUtils.Join(SSHKeyPair.KeyType.values(), ", ", + ", or ", " or ", null, (kt) -> kt.name()) + + ". Default: ED25519.") + .setUsageName("type") + .setOptional() + .setName('t', "type") + .setDefaultVal("ED25519") .setArgType(ArgumentBuilder.BuilderTypeNonFlag.STRING)); } @@ -1554,13 +1568,30 @@ public void execute(ArgumentParser.ArgumentParserResults parsedArgs) throws Exce File privOutputFile = new File(outputFileString); File pubOutputFile = new File(outputFileString + ".pub"); String label = parsedArgs.getStringArgument('l'); + String typeStr = parsedArgs.getStringArgument('t'); + if(typeStr == null || typeStr.isEmpty()) { + typeStr = "ED25519"; + } + SSHKeyPair.KeyType keyType; + try { + keyType = SSHKeyPair.KeyType.valueOf(typeStr.toUpperCase()); + } catch(IllegalArgumentException e) { + StreamUtils.GetSystemErr().println("Unknown key type: " + typeStr + + ". Supported types: " + + StringUtils.Join(SSHKeyPair.KeyType.values(), ", ", + ", or ", " or ", null, (kt) -> kt.name())); + System.exit(1); + return; + } if(privOutputFile.exists() || pubOutputFile.exists()) { - StreamUtils.GetSystemErr().println("Either the public key or private key file already exists. This utility will not overwrite any existing files."); + StreamUtils.GetSystemErr().println("Either the public key or private key" + + " file already exists. This utility will not overwrite any" + + " existing files."); System.exit(1); } - RSAEncrypt enc = RSAEncrypt.generateKey(label); - FileUtil.write(enc.getPrivateKey(), privOutputFile); - FileUtil.write(enc.getPublicKey(), pubOutputFile); + SSHKeyPair keyPair = SSHKeyPair.generateKey(keyType, label); + FileUtil.write(keyPair.getPrivateKeyPem(), privOutputFile); + FileUtil.write(keyPair.getPublicKeySsh(), pubOutputFile); System.exit(0); } } diff --git a/src/main/java/com/laytonsmith/tools/debugger/DebugAuthenticator.java b/src/main/java/com/laytonsmith/tools/debugger/DebugAuthenticator.java index 22730656a0..c50aa0b0c4 100644 --- a/src/main/java/com/laytonsmith/tools/debugger/DebugAuthenticator.java +++ b/src/main/java/com/laytonsmith/tools/debugger/DebugAuthenticator.java @@ -1,6 +1,6 @@ package com.laytonsmith.tools.debugger; -import com.laytonsmith.PureUtilities.Common.RSAEncrypt; +import com.laytonsmith.PureUtilities.Common.SSHKeyPair; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -12,7 +12,6 @@ import java.nio.file.Files; import java.security.SecureRandom; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; /** @@ -21,9 +20,9 @@ * The handshake occurs on the raw TCP socket before any DAP traffic. The protocol is: * * Server sends: magic bytes ({@code MSDBG\1}), then a 32-byte random nonce - * Client sends: the nonce signed with its private key, plus its public key string + * Client sends: a digital signature of the nonce, plus its public key string * Server verifies: (a) the public key is in the authorized keys file, - * (b) the signature decrypts to the original nonce + * (b) the signature is valid for the nonce * Server sends: a result byte (1 = success, 0 = failure) and an error message on failure * * @@ -134,9 +133,8 @@ public boolean authenticate(InputStream in, OutputStream out) throws IOException // Step 4: Verify the signature try { - RSAEncrypt rsa = new RSAEncrypt(null, clientPublicKey); - byte[] decrypted = rsa.decryptWithPublic(signature); - if(!Arrays.equals(nonce, decrypted)) { + SSHKeyPair keyPair = new SSHKeyPair(null, clientPublicKey); + if(!keyPair.verify(nonce, signature)) { sendFailure(dataOut, "Signature verification failed"); return false; } @@ -176,7 +174,7 @@ private boolean isAuthorized(String clientPublicKey) { /** * Extracts the key type and base64 key data from an SSH public key string, - * ignoring the label. Returns {@code "ssh-rsa "} or {@code null} if invalid. + * ignoring the label. Returns {@code "type "} or {@code null} if invalid. */ private static String extractKeyData(String sshPublicKey) { if(sshPublicKey == null) { diff --git a/src/main/java/com/laytonsmith/tools/langserv/LangServ.java b/src/main/java/com/laytonsmith/tools/langserv/LangServ.java index 812f88a486..732dde98c0 100644 --- a/src/main/java/com/laytonsmith/tools/langserv/LangServ.java +++ b/src/main/java/com/laytonsmith/tools/langserv/LangServ.java @@ -397,9 +397,11 @@ public CompletableFuture initialize(InitializeParams params) { sc.setWorkspace(wsc); sc.setDocumentSymbolProvider(true); - sc.setDeclarationProvider(true); - sc.setDefinitionProvider(true); - sc.setHoverProvider(true); + if(StaticAnalysis.enabled()) { + sc.setDeclarationProvider(true); + sc.setDefinitionProvider(true); + sc.setHoverProvider(true); + } // sc.setTypeDefinitionProvider(true); { diff --git a/src/main/java/com/laytonsmith/tools/langserv/LangServModel.java b/src/main/java/com/laytonsmith/tools/langserv/LangServModel.java index 2a7486f6e4..e640e9045e 100644 --- a/src/main/java/com/laytonsmith/tools/langserv/LangServModel.java +++ b/src/main/java/com/laytonsmith/tools/langserv/LangServModel.java @@ -251,7 +251,6 @@ private void syncRebuild() { // These may be present in the runtime environment, // but it's not possible for us to tell that at this point. StaticAnalysis envSa = new StaticAnalysis(true); - envSa.setLocalEnable(true); env = Static.GenerateStandaloneEnvironment(false, EnumSet.of(RuntimeMode.CMDLINE), includeCache, envSa); // Make this configurable at some point. For now, however, we need this so we can get @@ -293,7 +292,6 @@ private void syncRebuild() { for(File f2 : mainFiles) { String uri = f2.toURI().toString(); StaticAnalysis staticAnalysis = new StaticAnalysis(true); - staticAnalysis.setLocalEnable(true); parseTrees.put(uri, doCompilation(uri, staticAnalysis, env, exceptions)); staticAnalysisMap.put(uri, staticAnalysis); } @@ -309,7 +307,6 @@ private void syncRebuild() { // This was not included, was dynamically included, or there was a compile exception. // Can only treat it as an isolated script at this point. StaticAnalysis staticAnalysis = new StaticAnalysis(true); - staticAnalysis.setLocalEnable(true); parseTrees.put(uri, doCompilation(uri, staticAnalysis, env, exceptions)); staticAnalysisMap.put(uri, staticAnalysis); } diff --git a/src/test/java/com/laytonsmith/PureUtilities/RSAEncryptTest.java b/src/test/java/com/laytonsmith/PureUtilities/RSAEncryptTest.java deleted file mode 100644 index b034123d9a..0000000000 --- a/src/test/java/com/laytonsmith/PureUtilities/RSAEncryptTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.laytonsmith.PureUtilities; - -import com.laytonsmith.PureUtilities.Common.RSAEncrypt; -import org.junit.After; -import org.junit.AfterClass; -import static org.junit.Assert.assertEquals; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; - -/** - * - */ -public class RSAEncryptTest { - - RSAEncrypt enc; - byte[] data; - String sData; - - public RSAEncryptTest() { - } - - @BeforeClass - public static void setUpClass() { - } - - @AfterClass - public static void tearDownClass() { - } - - @Before - public void setUp() throws Exception { - enc = RSAEncrypt.generateKey("label@label"); - sData = "the test string"; - data = sData.getBytes("UTF-8"); - } - - @After - public void tearDown() { - } - - @Test - public void testPubToPriv() throws Exception { - byte[] c = enc.encryptWithPublic(data); - String s = new String(enc.decryptWithPrivate(c), "UTF-8"); - assertEquals(sData, s); - } - - @Test - public void testPrivToPub() throws Exception { - byte[] c = enc.encryptWithPrivate(data); - String s = new String(enc.decryptWithPublic(c), "UTF-8"); - assertEquals(sData, s); - } - -} diff --git a/src/test/java/com/laytonsmith/PureUtilities/SSHKeyPairTest.java b/src/test/java/com/laytonsmith/PureUtilities/SSHKeyPairTest.java new file mode 100644 index 0000000000..a2e2680820 --- /dev/null +++ b/src/test/java/com/laytonsmith/PureUtilities/SSHKeyPairTest.java @@ -0,0 +1,96 @@ +package com.laytonsmith.PureUtilities; + +import com.laytonsmith.PureUtilities.Common.SSHKeyPair; +import com.laytonsmith.PureUtilities.Common.SSHKeyPair.KeyType; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import java.util.Arrays; +import java.util.Collection; + +/** + * Tests for SSHKeyPair supporting RSA, Ed25519, and ECDSA key types. + */ +@RunWith(Parameterized.class) +public class SSHKeyPairTest { + + @Parameters(name = "{0}") + public static Collection keyTypes() { + return Arrays.asList(new Object[][]{ + {KeyType.RSA}, + {KeyType.ED25519}, + {KeyType.ECDSA_256}, + }); + } + + private final KeyType keyType; + + public SSHKeyPairTest(KeyType keyType) { + this.keyType = keyType; + } + + @Test + public void testSignAndVerify() throws Exception { + SSHKeyPair pair = SSHKeyPair.generateKey(keyType, "test@host"); + byte[] data = "test data".getBytes("UTF-8"); + byte[] sig = pair.sign(data); + assertTrue(pair.verify(data, sig)); + } + + @Test + public void testRoundTripThroughSerialization() throws Exception { + SSHKeyPair original = SSHKeyPair.generateKey(keyType, "label@host"); + String privPem = original.getPrivateKeyPem(); + String pubSsh = original.getPublicKeySsh(); + assertTrue(pubSsh.startsWith(keyType.getSshName() + " ")); + assertTrue(pubSsh.endsWith(" label@host")); + + SSHKeyPair signer = new SSHKeyPair(privPem, null); + SSHKeyPair verifier = new SSHKeyPair(null, pubSsh); + byte[] data = "round trip test".getBytes("UTF-8"); + byte[] sig = signer.sign(data); + assertTrue(verifier.verify(data, sig)); + } + + @Test + public void testKeyTypeDetectedFromPrivateKeyOnly() throws Exception { + SSHKeyPair pair = SSHKeyPair.generateKey(keyType, "test"); + SSHKeyPair fromPriv = new SSHKeyPair(pair.getPrivateKeyPem(), null); + assertEquals(keyType, fromPriv.getKeyType()); + } + + @Test + public void testGetKeyType() { + SSHKeyPair pair = SSHKeyPair.generateKey(keyType, "test"); + assertEquals(keyType, pair.getKeyType()); + } + + @Test + public void testGetLabel() { + SSHKeyPair pair = SSHKeyPair.generateKey(keyType, "user@example.com"); + assertEquals("user@example.com", pair.getLabel()); + } + + @Test + public void testPrivateKeyPemFormat() { + SSHKeyPair pair = SSHKeyPair.generateKey(keyType, "test"); + String pem = pair.getPrivateKeyPem(); + assertTrue(pem.contains("-----BEGIN PRIVATE KEY-----")); + assertTrue(pem.contains("-----END PRIVATE KEY-----")); + } + + @Test + public void testKeyTypeFromSshName() { + for(KeyType kt : KeyType.values()) { + assertEquals(kt, KeyType.fromSshName(kt.getSshName())); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testKeyTypeFromSshNameUnknown() { + KeyType.fromSshName("ssh-unknown"); + } +}
Keys are stored in standard formats: + *
The private key should be PKCS#8 PEM format ({@code -----BEGIN PRIVATE KEY-----}). + * The public key should be OpenSSH format ({@code type base64 label}). + * + * @param privateKeyPem The private key PEM string, or null + * @param publicKeySsh The public key OpenSSH string, or null + * @throws IllegalArgumentException If a key string cannot be parsed + */ + public SSHKeyPair(String privateKeyPem, String publicKeySsh) throws IllegalArgumentException { + if(publicKeySsh != null) { + String[] split = publicKeySsh.trim().split("\\s+"); + if(split.length < 2) { + throw new IllegalArgumentException("Invalid public key format."); + } + this.keyType = KeyType.fromSshName(split[0]); + this.label = split.length >= 3 ? split[2] : ""; + this.publicKey = sshToPublicKey(keyType, split[1]); + } + + if(privateKeyPem != null) { + this.privateKey = parsePrivateKey(privateKeyPem); + if(this.keyType == null) { + this.keyType = detectKeyType(privateKey); + } + } + } + + private SSHKeyPair() { + // Used by generateKey + } + + /** + * Signs the given data with the private key. + * + * @param data The data to sign + * @return The signature bytes + */ + public byte[] sign(byte[] data) { + Objects.requireNonNull(privateKey, "Private key is required for signing"); + try { + Signature sig = Signature.getInstance(keyType.signatureAlgorithm); + sig.initSign(privateKey); + sig.update(data); + return sig.sign(); + } catch(NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Verifies a signature against the given data using the public key. + * + * @param data The original data that was signed + * @param signature The signature to verify + * @return true if the signature is valid + */ + public boolean verify(byte[] data, byte[] signature) { + Objects.requireNonNull(publicKey, "Public key is required for verification"); + try { + Signature sig = Signature.getInstance(keyType.signatureAlgorithm); + sig.initVerify(publicKey); + sig.update(data); + return sig.verify(signature); + } catch(NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Returns the private key as a PKCS#8 PEM string. + */ + public String getPrivateKeyPem() { + Objects.requireNonNull(privateKey); + String base64 = Base64.encodeBase64String(privateKey.getEncoded()); + StringBuilder sb = new StringBuilder(); + sb.append("-----BEGIN PRIVATE KEY-----"); + for(int i = 0; i < base64.length(); i++) { + if(i % 64 == 0) { + sb.append(StringUtils.nl()); + } + sb.append(base64.charAt(i)); + } + sb.append(StringUtils.nl()).append("-----END PRIVATE KEY-----").append(StringUtils.nl()); + return sb.toString(); + } + + /** + * Returns the public key as an OpenSSH format string. + */ + public String getPublicKeySsh() { + Objects.requireNonNull(publicKey); + return publicKeyToSsh(keyType, publicKey, label); + } + + /** + * Returns the key type. + */ + public KeyType getKeyType() { + return keyType; + } + + /** + * Returns the label on the public key. + */ + public String getLabel() { + return label; + } + + // --- Private key parsing --- + + private static PrivateKey parsePrivateKey(String pem) { + pem = pem.replaceAll("\r", "").replaceAll("\n", ""); + // Strip all known PEM headers + pem = pem.replace("-----BEGIN PRIVATE KEY-----", ""); + pem = pem.replace("-----END PRIVATE KEY-----", ""); + pem = pem.replace("-----BEGIN RSA PRIVATE KEY-----", ""); + pem = pem.replace("-----END RSA PRIVATE KEY-----", ""); + pem = pem.replace("-----BEGIN EC PRIVATE KEY-----", ""); + pem = pem.replace("-----END EC PRIVATE KEY-----", ""); + pem = pem.trim(); + byte[] keyBytes = Base64.decodeBase64(pem); + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); + + // Try each algorithm until one works + String[] algorithms = {"Ed25519", "EC", "RSA"}; + for(String alg : algorithms) { + try { + KeyFactory kf = KeyFactory.getInstance(alg); + return kf.generatePrivate(spec); + } catch(NoSuchAlgorithmException | InvalidKeySpecException ex) { + // Try next + } + } + throw new IllegalArgumentException("Failed to parse private key. " + + "Supported types: RSA, Ed25519, ECDSA (PKCS#8 format)."); + } + + private static KeyType detectKeyType(PrivateKey key) { + return switch(key.getAlgorithm()) { + case "RSA" -> KeyType.RSA; + case "Ed25519", "EdDSA" -> KeyType.ED25519; + case "EC" -> KeyType.ECDSA_256; + default -> throw new IllegalArgumentException( + "Unsupported private key algorithm: " + key.getAlgorithm()); + }; + } + + // --- OpenSSH public key encoding/decoding --- + + private static String publicKeyToSsh(KeyType type, PublicKey key, String label) { + Objects.requireNonNull(label); + try { + byte[] wireBytes = encodePublicKeyWire(type, key); + return type.sshName + " " + Base64.encodeBase64String(wireBytes) + " " + label; + } catch(IOException ex) { + throw new RuntimeException(ex); + } + } + + private static byte[] encodePublicKeyWire(KeyType type, PublicKey key) throws IOException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(buf); + byte[] typeBytes = type.sshName.getBytes("UTF-8"); + dos.writeInt(typeBytes.length); + dos.write(typeBytes); + + switch(type) { + case RSA: { + RSAPublicKey rsaKey = (RSAPublicKey) key; + byte[] eBytes = rsaKey.getPublicExponent().toByteArray(); + dos.writeInt(eBytes.length); + dos.write(eBytes); + byte[] nBytes = rsaKey.getModulus().toByteArray(); + dos.writeInt(nBytes.length); + dos.write(nBytes); + break; + } + case ED25519: { + // Ed25519 public key is the raw 32-byte key from X.509 encoding + byte[] rawKey = extractEd25519RawPublicKey(key); + dos.writeInt(rawKey.length); + dos.write(rawKey); + break; + } + case ECDSA_256: { + ECPublicKey ecKey = (ECPublicKey) key; + // Write curve identifier + byte[] curveBytes = "nistp256".getBytes("UTF-8"); + dos.writeInt(curveBytes.length); + dos.write(curveBytes); + // Write uncompressed point (04 || x || y) + byte[] point = encodeEcPoint(ecKey); + dos.writeInt(point.length); + dos.write(point); + break; + } + default: + throw new UnsupportedOperationException("Unknown key type: " + type); + } + dos.flush(); + return buf.toByteArray(); + } + + private static PublicKey sshToPublicKey(KeyType type, String base64Part) { + byte[] decoded = Base64.decodeBase64(base64Part); + ByteBuffer bb = ByteBuffer.wrap(decoded); + // Skip key type string + int typeLen = bb.getInt(); + byte[] typeBytes = new byte[typeLen]; + bb.get(typeBytes); + + try { + return switch(type) { + case RSA -> decodeRsaPublicKey(bb); + case ED25519 -> decodeEd25519PublicKey(bb); + case ECDSA_256 -> decodeEcdsaPublicKey(bb); + }; + } catch(NoSuchAlgorithmException | InvalidKeySpecException ex) { + throw new RuntimeException("Failed to parse " + type.sshName + " public key", ex); + } + } + + private static PublicKey decodeRsaPublicKey(ByteBuffer bb) + throws NoSuchAlgorithmException, InvalidKeySpecException { + int eLen = bb.getInt(); + byte[] eBytes = new byte[eLen]; + bb.get(eBytes); + BigInteger e = new BigInteger(eBytes); + int nLen = bb.getInt(); + byte[] nBytes = new byte[nLen]; + bb.get(nBytes); + BigInteger n = new BigInteger(nBytes); + java.security.spec.RSAPublicKeySpec spec = new java.security.spec.RSAPublicKeySpec(n, e); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePublic(spec); + } + + private static PublicKey decodeEd25519PublicKey(ByteBuffer bb) + throws NoSuchAlgorithmException, InvalidKeySpecException { + int keyLen = bb.getInt(); + byte[] rawKey = new byte[keyLen]; + bb.get(rawKey); + // Wrap raw 32 bytes into X.509 SubjectPublicKeyInfo for Ed25519 + // The X.509 prefix for Ed25519 is fixed: 30 2a 30 05 06 03 2b 65 70 03 21 00 + byte[] x509Prefix = { + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00 + }; + byte[] x509Key = new byte[x509Prefix.length + rawKey.length]; + System.arraycopy(x509Prefix, 0, x509Key, 0, x509Prefix.length); + System.arraycopy(rawKey, 0, x509Key, x509Prefix.length, rawKey.length); + X509EncodedKeySpec spec = new X509EncodedKeySpec(x509Key); + KeyFactory kf = KeyFactory.getInstance("Ed25519"); + return kf.generatePublic(spec); + } + + private static PublicKey decodeEcdsaPublicKey(ByteBuffer bb) + throws NoSuchAlgorithmException, InvalidKeySpecException { + // Skip curve identifier string + int curveLen = bb.getInt(); + byte[] curveBytes = new byte[curveLen]; + bb.get(curveBytes); + // Read uncompressed EC point + int pointLen = bb.getInt(); + byte[] pointBytes = new byte[pointLen]; + bb.get(pointBytes); + java.security.spec.ECPoint point = decodeEcPoint(pointBytes); + java.security.spec.ECPublicKeySpec spec = new java.security.spec.ECPublicKeySpec( + point, getP256Params()); + KeyFactory kf = KeyFactory.getInstance("EC"); + return kf.generatePublic(spec); + } + + // --- EC point encoding/decoding helpers --- + + private static byte[] encodeEcPoint(ECPublicKey key) { + byte[] x = key.getW().getAffineX().toByteArray(); + byte[] y = key.getW().getAffineY().toByteArray(); + // Pad/trim to 32 bytes each for P-256 + x = padOrTrimTo(x, 32); + y = padOrTrimTo(y, 32); + byte[] result = new byte[1 + 32 + 32]; + result[0] = 0x04; // uncompressed + System.arraycopy(x, 0, result, 1, 32); + System.arraycopy(y, 0, result, 33, 32); + return result; + } + + private static java.security.spec.ECPoint decodeEcPoint(byte[] data) { + if(data[0] != 0x04) { + throw new IllegalArgumentException("Only uncompressed EC points are supported"); + } + int coordLen = (data.length - 1) / 2; + byte[] xBytes = new byte[coordLen]; + byte[] yBytes = new byte[coordLen]; + System.arraycopy(data, 1, xBytes, 0, coordLen); + System.arraycopy(data, 1 + coordLen, yBytes, 0, coordLen); + return new java.security.spec.ECPoint(new BigInteger(1, xBytes), new BigInteger(1, yBytes)); + } + + private static java.security.spec.ECParameterSpec getP256Params() { + try { + java.security.AlgorithmParameters params = + java.security.AlgorithmParameters.getInstance("EC"); + params.init(new ECGenParameterSpec("secp256r1")); + return params.getParameterSpec(java.security.spec.ECParameterSpec.class); + } catch(Exception ex) { + throw new RuntimeException(ex); + } + } + + private static byte[] padOrTrimTo(byte[] input, int length) { + if(input.length == length) { + return input; + } + byte[] result = new byte[length]; + if(input.length > length) { + // Trim leading bytes (BigInteger may have a leading zero for sign) + System.arraycopy(input, input.length - length, result, 0, length); + } else { + // Pad with leading zeros + System.arraycopy(input, 0, result, length - input.length, input.length); + } + return result; + } + + /** + * Extracts the raw 32-byte Ed25519 public key from the X.509 encoding. + */ + private static byte[] extractEd25519RawPublicKey(PublicKey key) { + byte[] x509 = key.getEncoded(); + // X.509 for Ed25519: 12-byte prefix + 32-byte raw key + byte[] raw = new byte[32]; + System.arraycopy(x509, x509.length - 32, raw, 0, 32); + return raw; + } +} diff --git a/src/main/java/com/laytonsmith/core/Main.java b/src/main/java/com/laytonsmith/core/Main.java index 74f74f162a..49ea66ffcf 100644 --- a/src/main/java/com/laytonsmith/core/Main.java +++ b/src/main/java/com/laytonsmith/core/Main.java @@ -9,7 +9,7 @@ import com.laytonsmith.PureUtilities.Common.ArrayUtils; import com.laytonsmith.PureUtilities.Common.FileUtil; import com.laytonsmith.PureUtilities.Common.OSUtils; -import com.laytonsmith.PureUtilities.Common.RSAEncrypt; +import com.laytonsmith.PureUtilities.Common.SSHKeyPair; import com.laytonsmith.PureUtilities.Common.StreamUtils; import com.laytonsmith.PureUtilities.Common.StringUtils; import com.laytonsmith.PureUtilities.Common.UIUtils; @@ -1524,27 +1524,41 @@ public void execute(ArgumentParser.ArgumentParserResults parsedArgs) throws Exce } @tool("key-gen") - public static class RSAKeyGenMode extends AbstractCommandLineTool { + public static class KeyGenMode extends AbstractCommandLineTool { @Override public ArgumentParser getArgumentParser() { return ArgumentParser.GetParser() - .addDescription("Creates an ssh compatible rsa key pair, suitable for use with the" + .addDescription("Creates an ssh compatible key pair, suitable for use with the" + " MethodScript debugger's KEYPAIR security mode, or other tools that" - + " require RSA key authentication.") + + " require key authentication. Supports " + + StringUtils.Join(SSHKeyPair.KeyType.values(), ", ", + ", or ", " or ", null, (kt) -> kt.name()) + + " key types.") .addArgument(new ArgumentBuilder() - .setDescription("Output file for the keys. For instance, \"/home/user/.ssh/id_rsa\"." + .setDescription("Output file for the keys. For instance," + + " \"/home/user/.ssh/id_ed25519\"." + " The public key will have the same name, with \".pub\" appended.") .setUsageName("file") .setRequired() .setName('o', "output-file") .setArgType(ArgumentBuilder.BuilderTypeNonFlag.STRING)) .addArgument(new ArgumentBuilder() - .setDescription("Label for the public key. For instance, \"user@localhost\" or an email" - + " address.") + .setDescription("Label for the public key. For instance," + + " \"user@localhost\" or an email address.") .setUsageName("label") .setRequired() .setName('l', "label") + .setArgType(ArgumentBuilder.BuilderTypeNonFlag.STRING)) + .addArgument(new ArgumentBuilder() + .setDescription("Key type to generate. Options: " + + StringUtils.Join(SSHKeyPair.KeyType.values(), ", ", + ", or ", " or ", null, (kt) -> kt.name()) + + ". Default: ED25519.") + .setUsageName("type") + .setOptional() + .setName('t', "type") + .setDefaultVal("ED25519") .setArgType(ArgumentBuilder.BuilderTypeNonFlag.STRING)); } @@ -1554,13 +1568,30 @@ public void execute(ArgumentParser.ArgumentParserResults parsedArgs) throws Exce File privOutputFile = new File(outputFileString); File pubOutputFile = new File(outputFileString + ".pub"); String label = parsedArgs.getStringArgument('l'); + String typeStr = parsedArgs.getStringArgument('t'); + if(typeStr == null || typeStr.isEmpty()) { + typeStr = "ED25519"; + } + SSHKeyPair.KeyType keyType; + try { + keyType = SSHKeyPair.KeyType.valueOf(typeStr.toUpperCase()); + } catch(IllegalArgumentException e) { + StreamUtils.GetSystemErr().println("Unknown key type: " + typeStr + + ". Supported types: " + + StringUtils.Join(SSHKeyPair.KeyType.values(), ", ", + ", or ", " or ", null, (kt) -> kt.name())); + System.exit(1); + return; + } if(privOutputFile.exists() || pubOutputFile.exists()) { - StreamUtils.GetSystemErr().println("Either the public key or private key file already exists. This utility will not overwrite any existing files."); + StreamUtils.GetSystemErr().println("Either the public key or private key" + + " file already exists. This utility will not overwrite any" + + " existing files."); System.exit(1); } - RSAEncrypt enc = RSAEncrypt.generateKey(label); - FileUtil.write(enc.getPrivateKey(), privOutputFile); - FileUtil.write(enc.getPublicKey(), pubOutputFile); + SSHKeyPair keyPair = SSHKeyPair.generateKey(keyType, label); + FileUtil.write(keyPair.getPrivateKeyPem(), privOutputFile); + FileUtil.write(keyPair.getPublicKeySsh(), pubOutputFile); System.exit(0); } } diff --git a/src/main/java/com/laytonsmith/tools/debugger/DebugAuthenticator.java b/src/main/java/com/laytonsmith/tools/debugger/DebugAuthenticator.java index 22730656a0..c50aa0b0c4 100644 --- a/src/main/java/com/laytonsmith/tools/debugger/DebugAuthenticator.java +++ b/src/main/java/com/laytonsmith/tools/debugger/DebugAuthenticator.java @@ -1,6 +1,6 @@ package com.laytonsmith.tools.debugger; -import com.laytonsmith.PureUtilities.Common.RSAEncrypt; +import com.laytonsmith.PureUtilities.Common.SSHKeyPair; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -12,7 +12,6 @@ import java.nio.file.Files; import java.security.SecureRandom; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; /** @@ -21,9 +20,9 @@ *
The handshake occurs on the raw TCP socket before any DAP traffic. The protocol is: *