mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12: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.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()
|
||||||
OpenSSHKeyPairResourceWriter.INSTANCE
|
if (ohKeyPair.type == "RSA") {
|
||||||
.writePublicKey(RSA.generatePublic(ohKeyPair.publicKey.decodeBase64()), null, baos)
|
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()
|
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
|
||||||
typeComboBox.addItem("RSA")
|
if (keyType == KeyPairProvider.SSH_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"))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user