diff --git a/src/main/kotlin/app/termora/keymgr/ByteArrayIoResource.kt b/src/main/kotlin/app/termora/keymgr/ByteArrayIoResource.kt new file mode 100644 index 0000000..f7fbedd --- /dev/null +++ b/src/main/kotlin/app/termora/keymgr/ByteArrayIoResource.kt @@ -0,0 +1,11 @@ +package app.termora.keymgr + +import org.apache.sshd.common.util.io.resource.AbstractIoResource +import java.io.ByteArrayInputStream +import java.io.InputStream + +class ByteArrayIoResource(bytes: ByteArray) : AbstractIoResource(ByteArray::class.java, bytes) { + override fun openInputStream(): InputStream { + return ByteArrayInputStream(resourceValue) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt b/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt index 4c6d1d9..216f7bc 100644 --- a/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt +++ b/src/main/kotlin/app/termora/keymgr/KeyManagerPanel.kt @@ -11,6 +11,7 @@ import com.formdev.flatlaf.ui.FlatTextBorder import com.formdev.flatlaf.util.SystemInfo import com.jgoodies.forms.builder.FormBuilder import com.jgoodies.forms.layout.FormLayout +import net.i2p.crypto.eddsa.EdDSAPublicKey import org.apache.commons.codec.binary.Base64 import org.apache.commons.io.IOUtils import org.apache.commons.io.file.PathUtils @@ -30,6 +31,7 @@ import java.io.File import java.nio.charset.StandardCharsets import java.nio.file.Files import java.security.KeyPair +import java.security.spec.X509EncodedKeySpec import java.util.* import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream @@ -187,8 +189,9 @@ class KeyManagerPanel : JPanel(BorderLayout()) { for (keyPair in keyPairs) { val pubNameCount = names.getOrPut(keyPair.name + ".pub") { 0 } val priNameCount = names.getOrPut(keyPair.name) { 0 } - val publicKey = RSA.generatePublic(Base64.decodeBase64(keyPair.publicKey)) - val privateKey = RSA.generatePrivate(Base64.decodeBase64(keyPair.privateKey)) + val kp = OhKeyPairKeyPairProvider.generateKeyPair(keyPair) + val publicKey = kp.public + val privateKey = kp.private zos.putNextEntry(ZipEntry("${keyPair.name}${if (pubNameCount > 0) ".${pubNameCount}" else String()}.pub")) OpenSSHKeyPairResourceWriter.INSTANCE.writePublicKey(publicKey, null, zos) @@ -236,6 +239,9 @@ class KeyManagerPanel : JPanel(BorderLayout()) { title = I18n.getString("termora.keymgr.title") typeComboBox.addItem("RSA") + typeComboBox.addItem("ED25519") + + // 默认 RSA lengthComboBox.addItem(1024) lengthComboBox.addItem(1024 * 2) lengthComboBox.addItem(1024 * 3) @@ -254,16 +260,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) { savePublicKeyBtn.isEnabled = false - savePublicKeyBtn.addActionListener { - val fileChooser = FileChooser() - fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY - fileChooser.win32Filters.add(Pair("All Files", listOf("*"))) - fileChooser.showSaveDialog(this, nameTextField.text).thenAccept { file -> - file?.outputStream()?.use { - IOUtils.write(publicKeyTextArea.text, it, StandardCharsets.UTF_8) - } - } - } + initEvents() if (editable) { typeComboBox.isEnabled = false @@ -273,8 +270,15 @@ class KeyManagerPanel : JPanel(BorderLayout()) { nameTextField.text = ohKeyPair.name remarkTextField.text = ohKeyPair.remark val baos = ByteArrayOutputStream() - OpenSSHKeyPairResourceWriter.INSTANCE - .writePublicKey(RSA.generatePublic(ohKeyPair.publicKey.decodeBase64()), null, baos) + if (ohKeyPair.type == "RSA") { + OpenSSHKeyPairResourceWriter.INSTANCE + .writePublicKey(RSA.generatePublic(ohKeyPair.publicKey.decodeBase64()), null, baos) + } else if (ohKeyPair.type == "ED25519") { + OpenSSHKeyPairResourceWriter.INSTANCE.writePublicKey( + EdDSAPublicKey(X509EncodedKeySpec(ohKeyPair.publicKey.decodeBase64())), + null, baos + ) + } publicKeyTextArea.text = baos.toString() savePublicKeyBtn.isEnabled = true } else { @@ -327,6 +331,35 @@ class KeyManagerPanel : JPanel(BorderLayout()) { .build() } + private fun initEvents() { + + savePublicKeyBtn.addActionListener { + val fileChooser = FileChooser() + fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY + fileChooser.win32Filters.add(Pair("All Files", listOf("*"))) + fileChooser.showSaveDialog(this, "${nameTextField.text}.pub").thenAccept { file -> + file?.outputStream()?.use { + IOUtils.write(publicKeyTextArea.text, it, StandardCharsets.UTF_8) + } + } + } + + typeComboBox.addItemListener { + if (it.stateChange == ItemEvent.SELECTED) { + lengthComboBox.removeAllItems() + if (typeComboBox.selectedItem == "ED25519") { + lengthComboBox.addItem(256) + } else if (typeComboBox.selectedItem == "RSA") { + lengthComboBox.addItem(1024) + lengthComboBox.addItem(1024 * 2) + lengthComboBox.addItem(1024 * 3) + lengthComboBox.addItem(1024 * 4) + lengthComboBox.addItem(1024 * 8) + lengthComboBox.selectedItem = 1024 * 2 + } + } + } + } override fun createOkAction(): AbstractAction { if (!editable) { @@ -349,7 +382,9 @@ class KeyManagerPanel : JPanel(BorderLayout()) { return } - val keyPair = RSA.generateKeyPair(lengthComboBox.selectedItem as Int) + val keyType = if (typeComboBox.selectedItem == "RSA") + KeyPairProvider.SSH_RSA else KeyPairProvider.SSH_ED25519 + val keyPair = KeyUtils.generateKeyPair(keyType, lengthComboBox.selectedItem as Int) ohKeyPair = OhKeyPair( id = UUID.randomUUID().toSimpleString(), name = nameTextField.text, @@ -516,16 +551,20 @@ class KeyManagerPanel : JPanel(BorderLayout()) { val dialog = InputDialog(owner = this@ImportKeyDialog, title = "Password") dialog.getText() ?: String() } - val keyPair = - provider.loadKeys(null).firstOrNull() ?: throw IllegalStateException("Failed to load the key file") + val keyPair = provider.loadKeys(null).firstOrNull() + ?: throw IllegalStateException("Failed to load the key file") val keyType = KeyUtils.getKeyType(keyPair) - if (keyType != KeyPairProvider.SSH_RSA) { - throw UnsupportedOperationException("Key type:${keyType}. Only RSA keys are supported.") + if (keyType != KeyPairProvider.SSH_RSA && keyType != KeyPairProvider.SSH_ED25519) { + throw UnsupportedOperationException("Key type:${keyType}. Only RSA/ED25519 keys are supported.") } nameTextField.text = StringUtils.defaultIfBlank(nameTextField.text, file.name) fileTextField.text = file.absolutePath - typeComboBox.addItem("RSA") + if (keyType == KeyPairProvider.SSH_RSA) { + typeComboBox.addItem("RSA") + } else { + typeComboBox.addItem("ED25519") + } lengthComboBox.addItem(KeyUtils.getKeySize(keyPair.private)) ohKeyPair = OhKeyPair( @@ -573,6 +612,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) { if (ohKeyPair.remark.isEmpty()) { ohKeyPair = ohKeyPair.copy( + name = nameTextField.text, remark = "Import on " + DateFormatUtils.format(Date(), I18n.getString("termora.date-format")) ) } diff --git a/src/main/kotlin/app/termora/keymgr/OhKeyPair.kt b/src/main/kotlin/app/termora/keymgr/OhKeyPair.kt index 328fa69..f36ddf7 100644 --- a/src/main/kotlin/app/termora/keymgr/OhKeyPair.kt +++ b/src/main/kotlin/app/termora/keymgr/OhKeyPair.kt @@ -9,7 +9,7 @@ data class OhKeyPair( val publicKey: String, // base64 val privateKey: String, - // RSA + // RSA、ED25519 val type: String, val name: String, val remark: String, diff --git a/src/main/kotlin/app/termora/keymgr/OhKeyPairKeyPairProvider.kt b/src/main/kotlin/app/termora/keymgr/OhKeyPairKeyPairProvider.kt index 2c43b5d..47870bf 100644 --- a/src/main/kotlin/app/termora/keymgr/OhKeyPairKeyPairProvider.kt +++ b/src/main/kotlin/app/termora/keymgr/OhKeyPairKeyPairProvider.kt @@ -2,6 +2,8 @@ package app.termora.keymgr import app.termora.AES.decodeBase64 import app.termora.RSA +import net.i2p.crypto.eddsa.EdDSAPrivateKey +import net.i2p.crypto.eddsa.EdDSAPublicKey import org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider import org.apache.sshd.common.session.SessionContext import org.slf4j.LoggerFactory @@ -9,6 +11,8 @@ import java.security.Key import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -16,6 +20,27 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair companion object { private val log = LoggerFactory.getLogger(OhKeyPairKeyPairProvider::class.java) private val cache = ConcurrentHashMap() + + fun generateKeyPair(ohKeyPair: OhKeyPair): KeyPair { + val publicKey = cache.getOrPut(ohKeyPair.publicKey) { + when (ohKeyPair.type) { + "RSA" -> RSA.generatePublic(ohKeyPair.publicKey.decodeBase64()) + "ED25519" -> EdDSAPublicKey(X509EncodedKeySpec(ohKeyPair.publicKey.decodeBase64())) + else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported") + } + } as PublicKey + + val privateKey = cache.getOrPut(ohKeyPair.privateKey) { + when (ohKeyPair.type) { + "RSA" -> RSA.generatePrivate(ohKeyPair.privateKey.decodeBase64()) + "ED25519" -> EdDSAPrivateKey(PKCS8EncodedKeySpec(ohKeyPair.privateKey.decodeBase64())) + else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported") + } + } as PrivateKey + + return KeyPair(publicKey, privateKey) + + } } @@ -31,13 +56,7 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair return object : Iterable { override fun iterator(): Iterator { - val result = kotlin.runCatching { - val publicKey = cache.getOrPut(ohKeyPair.publicKey) - { RSA.generatePublic(ohKeyPair.publicKey.decodeBase64()) } as PublicKey - val privateKey = cache.getOrPut(ohKeyPair.privateKey) - { RSA.generatePrivate(ohKeyPair.privateKey.decodeBase64()) } as PrivateKey - return@runCatching KeyPair(publicKey, privateKey) - } + val result = kotlin.runCatching { generateKeyPair(ohKeyPair) } if (result.isSuccess) { return listOf(result.getOrThrow()).iterator() } else if (log.isErrorEnabled) { diff --git a/src/test/kotlin/app/termora/KeyUtilsTest.kt b/src/test/kotlin/app/termora/KeyUtilsTest.kt index f23b269..6e93f38 100644 --- a/src/test/kotlin/app/termora/KeyUtilsTest.kt +++ b/src/test/kotlin/app/termora/KeyUtilsTest.kt @@ -1,8 +1,12 @@ package app.termora import org.apache.sshd.common.config.keys.KeyUtils +import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyPairResourceWriter +import org.apache.sshd.common.keyprovider.KeyPairProvider +import java.io.ByteArrayOutputStream import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue class KeyUtilsTest { @Test @@ -10,4 +14,28 @@ class KeyUtilsTest { assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair("ssh-rsa", 1024).private), 1024) assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair("ssh-rsa", 1024).public), 1024) } + + @Test + fun test_ed25519() { + val keyPair = KeyUtils.generateKeyPair(KeyPairProvider.SSH_ED25519, 256) + assertEquals(KeyUtils.getKeyType(keyPair), KeyPairProvider.SSH_ED25519) + assertEquals(KeyUtils.getKeySize(keyPair.private), 256) + assertEquals(KeyUtils.getKeySize(keyPair.public), 256) + + val baos = ByteArrayOutputStream() + + OpenSSHKeyPairResourceWriter.INSTANCE + .writePublicKey(keyPair.public, null, baos) + + assertTrue(baos.toString().startsWith(KeyPairProvider.SSH_ED25519)) + + baos.reset() + + OpenSSHKeyPairResourceWriter.INSTANCE + .writePrivateKey(keyPair, null, null, baos) + + println(baos.toString()) + + + } } \ No newline at end of file