mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: export configuration file support encryption (#221)
This commit is contained in:
@@ -35,6 +35,7 @@ import kotlinx.coroutines.*
|
|||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
|
import org.apache.commons.codec.binary.Base64
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
@@ -667,13 +668,40 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
|
|
||||||
private fun export() {
|
private fun export() {
|
||||||
|
|
||||||
|
assertEventDispatchThread()
|
||||||
|
|
||||||
|
val passwordField = OutlinePasswordField()
|
||||||
|
val panel = object : JPanel(BorderLayout()) {
|
||||||
|
override fun requestFocusInWindow(): Boolean {
|
||||||
|
return passwordField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val label = JLabel(I18n.getString("termora.settings.sync.export-encrypt") + StringUtils.SPACE.repeat(25))
|
||||||
|
label.border = BorderFactory.createEmptyBorder(0, 0, 8, 0)
|
||||||
|
panel.add(label, BorderLayout.NORTH)
|
||||||
|
panel.add(passwordField, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
var password = StringUtils.EMPTY
|
||||||
|
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
owner,
|
||||||
|
panel,
|
||||||
|
optionType = JOptionPane.YES_NO_OPTION,
|
||||||
|
initialValue = passwordField
|
||||||
|
) == JOptionPane.YES_OPTION
|
||||||
|
) {
|
||||||
|
password = String(passwordField.password).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val fileChooser = FileChooser()
|
val fileChooser = FileChooser()
|
||||||
fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||||
fileChooser.win32Filters.add(Pair("All Files", listOf("*")))
|
fileChooser.win32Filters.add(Pair("All Files", listOf("*")))
|
||||||
fileChooser.win32Filters.add(Pair("JSON files", listOf("json")))
|
fileChooser.win32Filters.add(Pair("JSON files", listOf("json")))
|
||||||
fileChooser.showSaveDialog(owner, "${Application.getName()}.json").thenAccept { file ->
|
fileChooser.showSaveDialog(owner, "${Application.getName()}.json").thenAccept { file ->
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
SwingUtilities.invokeLater { exportText(file) }
|
SwingUtilities.invokeLater { exportText(file, password) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -690,6 +718,7 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DuplicatedCode")
|
||||||
private fun importFromFile(file: File) {
|
private fun importFromFile(file: File) {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
return
|
return
|
||||||
@@ -720,7 +749,79 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val json = jsonResult.getOrNull() ?: return
|
var json = jsonResult.getOrNull() ?: return
|
||||||
|
|
||||||
|
// 如果加密了 则解密数据
|
||||||
|
if (json["encryption"]?.jsonPrimitive?.booleanOrNull == true) {
|
||||||
|
val data = json["data"]?.jsonPrimitive?.content ?: StringUtils.EMPTY
|
||||||
|
if (data.isBlank()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, "Data file corruption",
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val passwordField = OutlinePasswordField()
|
||||||
|
val panel = object : JPanel(BorderLayout()) {
|
||||||
|
override fun requestFocusInWindow(): Boolean {
|
||||||
|
return passwordField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val label = JLabel("Please enter the password" + StringUtils.SPACE.repeat(25))
|
||||||
|
label.border = BorderFactory.createEmptyBorder(0, 0, 8, 0)
|
||||||
|
panel.add(label, BorderLayout.NORTH)
|
||||||
|
panel.add(passwordField, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
owner,
|
||||||
|
panel,
|
||||||
|
optionType = JOptionPane.YES_NO_OPTION,
|
||||||
|
initialValue = passwordField
|
||||||
|
) != JOptionPane.YES_OPTION
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordField.password.isEmpty()) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, I18n.getString("termora.doorman.unlock-data"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val password = String(passwordField.password)
|
||||||
|
val key = PBKDF2.generateSecret(
|
||||||
|
password.toCharArray(),
|
||||||
|
password.toByteArray(), keyLength = 128
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val dataText = AES.ECB.decrypt(key, Base64.decodeBase64(data)).toString(Charsets.UTF_8)
|
||||||
|
val dataJsonResult = ohMyJson.runCatching { decodeFromString<JsonObject>(dataText) }
|
||||||
|
if (dataJsonResult.isFailure) {
|
||||||
|
val e = dataJsonResult.exceptionOrNull() ?: return
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, ExceptionUtils.getRootCauseMessage(e),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
json = dataJsonResult.getOrNull() ?: return
|
||||||
|
break
|
||||||
|
} catch (_: Exception) {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner, I18n.getString("termora.doorman.password-wrong"),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ranges.contains(SyncRange.Hosts)) {
|
if (ranges.contains(SyncRange.Hosts)) {
|
||||||
val hosts = json["hosts"]
|
val hosts = json["hosts"]
|
||||||
if (hosts is JsonArray) {
|
if (hosts is JsonArray) {
|
||||||
@@ -781,9 +882,9 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportText(file: File) {
|
private fun exportText(file: File, password: String) {
|
||||||
val syncConfig = getSyncConfig()
|
val syncConfig = getSyncConfig()
|
||||||
val text = ohMyJson.encodeToString(buildJsonObject {
|
var text = ohMyJson.encodeToString(buildJsonObject {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
put("exporter", SystemUtils.USER_NAME)
|
put("exporter", SystemUtils.USER_NAME)
|
||||||
put("version", Application.getVersion())
|
put("version", Application.getVersion())
|
||||||
@@ -822,6 +923,19 @@ class SettingsOptionsPane : OptionsPane() {
|
|||||||
put("terminal", ohMyJson.encodeToJsonElement(database.terminal.getProperties()))
|
put("terminal", ohMyJson.encodeToJsonElement(database.terminal.getProperties()))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (password.isNotBlank()) {
|
||||||
|
val key = PBKDF2.generateSecret(
|
||||||
|
password.toCharArray(),
|
||||||
|
password.toByteArray(), keyLength = 128
|
||||||
|
)
|
||||||
|
|
||||||
|
text = ohMyJson.encodeToString(buildJsonObject {
|
||||||
|
put("encryption", true)
|
||||||
|
put("data", AES.ECB.encrypt(key, text.toByteArray(Charsets.UTF_8)).encodeBase64String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
file.outputStream().use {
|
file.outputStream().use {
|
||||||
IOUtils.write(text, it, StandardCharsets.UTF_8)
|
IOUtils.write(text, it, StandardCharsets.UTF_8)
|
||||||
OptionPane.openFileInFolder(
|
OptionPane.openFileInFolder(
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ termora.settings.sync.import=${termora.keymgr.import}
|
|||||||
termora.settings.sync.import.file-too-large=The file is too large
|
termora.settings.sync.import.file-too-large=The file is too large
|
||||||
termora.settings.sync.import.successful=Import data successfully
|
termora.settings.sync.import.successful=Import data successfully
|
||||||
termora.settings.sync.export-done=The export was successful
|
termora.settings.sync.export-done=The export was successful
|
||||||
|
termora.settings.sync.export-encrypt=Enter password to encrypt file (optional)
|
||||||
termora.settings.sync.export-done-open-folder=The export was successful. Do you want to open the folder?
|
termora.settings.sync.export-done-open-folder=The export was successful. Do you want to open the folder?
|
||||||
termora.settings.sync.range=Range
|
termora.settings.sync.range=Range
|
||||||
termora.settings.sync.range.keys=My keys
|
termora.settings.sync.range.keys=My keys
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ termora.settings.sync.push=推送
|
|||||||
termora.settings.sync.push-warning=推送将覆盖已有配置,建议先拉取再推送
|
termora.settings.sync.push-warning=推送将覆盖已有配置,建议先拉取再推送
|
||||||
termora.settings.sync.pull=拉取
|
termora.settings.sync.pull=拉取
|
||||||
termora.settings.sync.export-done=导出成功
|
termora.settings.sync.export-done=导出成功
|
||||||
|
termora.settings.sync.export-encrypt=输入密码加密文件 (可选)
|
||||||
termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹?
|
termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹?
|
||||||
termora.settings.sync.range=范围
|
termora.settings.sync.range=范围
|
||||||
termora.settings.sync.range.keys=我的密钥
|
termora.settings.sync.range.keys=我的密钥
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ termora.settings.sync.push=推送
|
|||||||
termora.settings.sync.push-warning=推送將覆蓋先前的配置,建議先拉取再推送
|
termora.settings.sync.push-warning=推送將覆蓋先前的配置,建議先拉取再推送
|
||||||
termora.settings.sync.pull=拉取
|
termora.settings.sync.pull=拉取
|
||||||
termora.settings.sync.export-done=匯出成功
|
termora.settings.sync.export-done=匯出成功
|
||||||
|
termora.settings.sync.export-encrypt=輸入密碼加密檔案 (可選)
|
||||||
termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
|
termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾?
|
||||||
termora.settings.sync.range=範圍
|
termora.settings.sync.range=範圍
|
||||||
termora.settings.sync.range.keys=我的密鑰
|
termora.settings.sync.range.keys=我的密鑰
|
||||||
|
|||||||
Reference in New Issue
Block a user