diff --git a/src/main/kotlin/app/termora/SettingsOptionsPane.kt b/src/main/kotlin/app/termora/SettingsOptionsPane.kt index 314a969..ff48dc7 100644 --- a/src/main/kotlin/app/termora/SettingsOptionsPane.kt +++ b/src/main/kotlin/app/termora/SettingsOptionsPane.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.swing.Swing import kotlinx.serialization.encodeToString import kotlinx.serialization.json.* +import org.apache.commons.codec.binary.Base64 import org.apache.commons.io.IOUtils import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.SystemUtils @@ -667,13 +668,40 @@ class SettingsOptionsPane : OptionsPane() { 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() fileChooser.fileSelectionMode = JFileChooser.FILES_ONLY fileChooser.win32Filters.add(Pair("All Files", listOf("*"))) fileChooser.win32Filters.add(Pair("JSON files", listOf("json"))) fileChooser.showSaveDialog(owner, "${Application.getName()}.json").thenAccept { file -> 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) { if (!file.exists()) { return @@ -720,7 +749,79 @@ class SettingsOptionsPane : OptionsPane() { 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(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)) { val hosts = json["hosts"] 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 text = ohMyJson.encodeToString(buildJsonObject { + var text = ohMyJson.encodeToString(buildJsonObject { val now = System.currentTimeMillis() put("exporter", SystemUtils.USER_NAME) put("version", Application.getVersion()) @@ -822,6 +923,19 @@ class SettingsOptionsPane : OptionsPane() { 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 { IOUtils.write(text, it, StandardCharsets.UTF_8) OptionPane.openFileInFolder( diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 187def6..faef0b0 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -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.successful=Import data successfully 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.range=Range termora.settings.sync.range.keys=My keys diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 6a44489..a989437 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -86,6 +86,7 @@ termora.settings.sync.push=推送 termora.settings.sync.push-warning=推送将覆盖已有配置,建议先拉取再推送 termora.settings.sync.pull=拉取 termora.settings.sync.export-done=导出成功 +termora.settings.sync.export-encrypt=输入密码加密文件 (可选) termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹? termora.settings.sync.range=范围 termora.settings.sync.range.keys=我的密钥 diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index 0418274..5d31e2a 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -94,6 +94,7 @@ termora.settings.sync.push=推送 termora.settings.sync.push-warning=推送將覆蓋先前的配置,建議先拉取再推送 termora.settings.sync.pull=拉取 termora.settings.sync.export-done=匯出成功 +termora.settings.sync.export-encrypt=輸入密碼加密檔案 (可選) termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾? termora.settings.sync.range=範圍 termora.settings.sync.range.keys=我的密鑰