feat: 支持导入 ed25519 (#5)

This commit is contained in:
hstyi
2025-01-03 11:32:29 +08:00
committed by hstyi
parent 401712c5b5
commit fe1106658a
5 changed files with 126 additions and 28 deletions

View File

@@ -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>(ByteArray::class.java, bytes) {
override fun openInputStream(): InputStream {
return ByteArrayInputStream(resourceValue)
}
}

View File

@@ -11,6 +11,7 @@ import com.formdev.flatlaf.ui.FlatTextBorder
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jgoodies.forms.builder.FormBuilder import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout import com.jgoodies.forms.layout.FormLayout
import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.apache.commons.codec.binary.Base64 import org.apache.commons.codec.binary.Base64
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.apache.commons.io.file.PathUtils import org.apache.commons.io.file.PathUtils
@@ -30,6 +31,7 @@ import java.io.File
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.Files import java.nio.file.Files
import java.security.KeyPair import java.security.KeyPair
import java.security.spec.X509EncodedKeySpec
import java.util.* import java.util.*
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
@@ -187,8 +189,9 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
for (keyPair in keyPairs) { for (keyPair in keyPairs) {
val pubNameCount = names.getOrPut(keyPair.name + ".pub") { 0 } val pubNameCount = names.getOrPut(keyPair.name + ".pub") { 0 }
val priNameCount = names.getOrPut(keyPair.name) { 0 } val priNameCount = names.getOrPut(keyPair.name) { 0 }
val publicKey = RSA.generatePublic(Base64.decodeBase64(keyPair.publicKey)) val kp = OhKeyPairKeyPairProvider.generateKeyPair(keyPair)
val privateKey = RSA.generatePrivate(Base64.decodeBase64(keyPair.privateKey)) val publicKey = kp.public
val privateKey = kp.private
zos.putNextEntry(ZipEntry("${keyPair.name}${if (pubNameCount > 0) ".${pubNameCount}" else String()}.pub")) zos.putNextEntry(ZipEntry("${keyPair.name}${if (pubNameCount > 0) ".${pubNameCount}" else String()}.pub"))
OpenSSHKeyPairResourceWriter.INSTANCE.writePublicKey(publicKey, null, zos) OpenSSHKeyPairResourceWriter.INSTANCE.writePublicKey(publicKey, null, zos)
@@ -236,6 +239,9 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
title = I18n.getString("termora.keymgr.title") title = I18n.getString("termora.keymgr.title")
typeComboBox.addItem("RSA") typeComboBox.addItem("RSA")
typeComboBox.addItem("ED25519")
// 默认 RSA
lengthComboBox.addItem(1024) lengthComboBox.addItem(1024)
lengthComboBox.addItem(1024 * 2) lengthComboBox.addItem(1024 * 2)
lengthComboBox.addItem(1024 * 3) lengthComboBox.addItem(1024 * 3)
@@ -254,16 +260,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
savePublicKeyBtn.isEnabled = false savePublicKeyBtn.isEnabled = false
savePublicKeyBtn.addActionListener { initEvents()
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)
}
}
}
if (editable) { if (editable) {
typeComboBox.isEnabled = false typeComboBox.isEnabled = false
@@ -273,8 +270,15 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
nameTextField.text = ohKeyPair.name nameTextField.text = ohKeyPair.name
remarkTextField.text = ohKeyPair.remark remarkTextField.text = ohKeyPair.remark
val baos = ByteArrayOutputStream() val baos = ByteArrayOutputStream()
if (ohKeyPair.type == "RSA") {
OpenSSHKeyPairResourceWriter.INSTANCE OpenSSHKeyPairResourceWriter.INSTANCE
.writePublicKey(RSA.generatePublic(ohKeyPair.publicKey.decodeBase64()), null, baos) .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() publicKeyTextArea.text = baos.toString()
savePublicKeyBtn.isEnabled = true savePublicKeyBtn.isEnabled = true
} else { } else {
@@ -327,6 +331,35 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
.build() .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 { override fun createOkAction(): AbstractAction {
if (!editable) { if (!editable) {
@@ -349,7 +382,9 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
return 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( ohKeyPair = OhKeyPair(
id = UUID.randomUUID().toSimpleString(), id = UUID.randomUUID().toSimpleString(),
name = nameTextField.text, name = nameTextField.text,
@@ -516,16 +551,20 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
val dialog = InputDialog(owner = this@ImportKeyDialog, title = "Password") val dialog = InputDialog(owner = this@ImportKeyDialog, title = "Password")
dialog.getText() ?: String() dialog.getText() ?: String()
} }
val keyPair = val keyPair = provider.loadKeys(null).firstOrNull()
provider.loadKeys(null).firstOrNull() ?: throw IllegalStateException("Failed to load the key file") ?: throw IllegalStateException("Failed to load the key file")
val keyType = KeyUtils.getKeyType(keyPair) val keyType = KeyUtils.getKeyType(keyPair)
if (keyType != KeyPairProvider.SSH_RSA) { if (keyType != KeyPairProvider.SSH_RSA && keyType != KeyPairProvider.SSH_ED25519) {
throw UnsupportedOperationException("Key type:${keyType}. Only RSA keys are supported.") throw UnsupportedOperationException("Key type:${keyType}. Only RSA/ED25519 keys are supported.")
} }
nameTextField.text = StringUtils.defaultIfBlank(nameTextField.text, file.name) nameTextField.text = StringUtils.defaultIfBlank(nameTextField.text, file.name)
fileTextField.text = file.absolutePath fileTextField.text = file.absolutePath
if (keyType == KeyPairProvider.SSH_RSA) {
typeComboBox.addItem("RSA") typeComboBox.addItem("RSA")
} else {
typeComboBox.addItem("ED25519")
}
lengthComboBox.addItem(KeyUtils.getKeySize(keyPair.private)) lengthComboBox.addItem(KeyUtils.getKeySize(keyPair.private))
ohKeyPair = OhKeyPair( ohKeyPair = OhKeyPair(
@@ -573,6 +612,7 @@ class KeyManagerPanel : JPanel(BorderLayout()) {
if (ohKeyPair.remark.isEmpty()) { if (ohKeyPair.remark.isEmpty()) {
ohKeyPair = ohKeyPair.copy( ohKeyPair = ohKeyPair.copy(
name = nameTextField.text,
remark = "Import on " + DateFormatUtils.format(Date(), I18n.getString("termora.date-format")) remark = "Import on " + DateFormatUtils.format(Date(), I18n.getString("termora.date-format"))
) )
} }

View File

@@ -9,7 +9,7 @@ data class OhKeyPair(
val publicKey: String, val publicKey: String,
// base64 // base64
val privateKey: String, val privateKey: String,
// RSA // RSA、ED25519
val type: String, val type: String,
val name: String, val name: String,
val remark: String, val remark: String,

View File

@@ -2,6 +2,8 @@ package app.termora.keymgr
import app.termora.AES.decodeBase64 import app.termora.AES.decodeBase64
import app.termora.RSA 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.keyprovider.AbstractResourceKeyPairProvider
import org.apache.sshd.common.session.SessionContext import org.apache.sshd.common.session.SessionContext
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -9,6 +11,8 @@ import java.security.Key
import java.security.KeyPair import java.security.KeyPair
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey import java.security.PublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -16,6 +20,27 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair
companion object { companion object {
private val log = LoggerFactory.getLogger(OhKeyPairKeyPairProvider::class.java) private val log = LoggerFactory.getLogger(OhKeyPairKeyPairProvider::class.java)
private val cache = ConcurrentHashMap<String, Key>() private val cache = ConcurrentHashMap<String, Key>()
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<KeyPair> { return object : Iterable<KeyPair> {
override fun iterator(): Iterator<KeyPair> { override fun iterator(): Iterator<KeyPair> {
val result = kotlin.runCatching { val result = kotlin.runCatching { generateKeyPair(ohKeyPair) }
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)
}
if (result.isSuccess) { if (result.isSuccess) {
return listOf(result.getOrThrow()).iterator() return listOf(result.getOrThrow()).iterator()
} else if (log.isErrorEnabled) { } else if (log.isErrorEnabled) {

View File

@@ -1,8 +1,12 @@
package app.termora package app.termora
import org.apache.sshd.common.config.keys.KeyUtils 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.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue
class KeyUtilsTest { class KeyUtilsTest {
@Test @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).private), 1024)
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair("ssh-rsa", 1024).public), 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())
}
} }