mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
feat: 支持导入 ed25519 (#5)
This commit is contained in:
11
src/main/kotlin/app/termora/keymgr/ByteArrayIoResource.kt
Normal file
11
src/main/kotlin/app/termora/keymgr/ByteArrayIoResource.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
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
|
||||
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"))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<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> {
|
||||
override fun iterator(): Iterator<KeyPair> {
|
||||
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) {
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user