chore: improve database secret

This commit is contained in:
hstyi
2025-06-13 15:57:48 +08:00
committed by hstyi
parent 6177bbdc68
commit ab017be855
7 changed files with 157 additions and 55 deletions

View File

@@ -1,29 +0,0 @@
package app.termora
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.SystemUtils
/**
* 用户需要保证自己的电脑是可信环境
*/
internal class LocalSecret private constructor() {
companion object {
fun getInstance(): LocalSecret {
return ApplicationScope.forApplicationScope()
.getOrCreate(LocalSecret::class) { LocalSecret() }
}
}
/**
* 一个 16 长度的密码
*/
val password: String = StringUtils.substring(DigestUtils.sha256Hex(SystemUtils.USER_NAME), 0, 16)
/**
* 一个 12 长度的盐
*/
val salt: String = StringUtils.substring(password, 0, 16)
}

View File

@@ -16,12 +16,13 @@ import org.jdesktop.swingx.JXHyperlink
import java.awt.Component import java.awt.Component
import java.awt.Dimension import java.awt.Dimension
import java.awt.Window import java.awt.Window
import java.awt.event.ItemEvent
import java.awt.event.WindowAdapter import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
import java.net.URI import java.net.URI
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.* import javax.swing.*
import javax.swing.event.ListDataEvent
import javax.swing.event.ListDataListener
import kotlin.math.max import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@@ -33,6 +34,10 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
private val cancelAction = super.createCancelAction() private val cancelAction = super.createCancelAction()
private val cancelButton = super.createJButtonForAction(cancelAction) private val cancelButton = super.createJButtonForAction(cancelAction)
private val isLoggingIn = AtomicBoolean(false) private val isLoggingIn = AtomicBoolean(false)
private val singaporeServer =
Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app")
private val chinaServer =
Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
init { init {
isModal = true isModal = true
@@ -63,16 +68,12 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
val step = 2 val step = 2
val singaporeServer =
Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app")
val chinaServer = Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
if (Application.isUnknownVersion()) { if (Application.isUnknownVersion()) {
serverComboBox.addItem(Server("Localhost", "http://127.0.0.1:8080")) serverComboBox.addItem(Server("Localhost", "http://127.0.0.1:8080"))
} }
serverComboBox.addItem(singaporeServer) // serverComboBox.addItem(singaporeServer)
serverComboBox.addItem(chinaServer) // serverComboBox.addItem(chinaServer)
val properties = DatabaseManager.getInstance().properties val properties = DatabaseManager.getInstance().properties
val servers = (runCatching { val servers = (runCatching {
@@ -117,7 +118,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
val dialog = this val dialog = this
val newAction = object : AnAction(I18n.getString("termora.welcome.contextmenu.new")) { val newAction = object : AnAction(I18n.getString("termora.welcome.contextmenu.new")) {
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
if (serverComboBox.selectedItem == singaporeServer || serverComboBox.selectedItem == chinaServer) { if (serverComboBox.itemCount < 1 || serverComboBox.selectedItem == singaporeServer || serverComboBox.selectedItem == chinaServer) {
val c = NewServerDialog(dialog) val c = NewServerDialog(dialog)
c.isVisible = true c.isVisible = true
val server = c.server ?: return val server = c.server ?: return
@@ -142,18 +143,35 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
} }
} }
} }
val newServer = JXHyperlink(newAction)
newServer.isFocusable = false
serverComboBox.addItemListener { fun refreshButton() {
if (it.stateChange == ItemEvent.SELECTED) { if (serverComboBox.selectedItem == singaporeServer || serverComboBox.selectedItem == chinaServer || serverComboBox.itemCount < 1) {
if (serverComboBox.selectedItem == singaporeServer || serverComboBox.selectedItem == chinaServer) {
newAction.name = I18n.getString("termora.welcome.contextmenu.new") newAction.name = I18n.getString("termora.welcome.contextmenu.new")
} else { } else {
newAction.name = I18n.getString("termora.remove") newAction.name = I18n.getString("termora.remove")
} }
} }
val newServer = JXHyperlink(newAction)
newServer.isFocusable = false
serverComboBox.addItemListener { refreshButton() }
serverComboBox.model.addListDataListener(object : ListDataListener {
override fun intervalAdded(e: ListDataEvent?) {
refreshButton()
} }
override fun intervalRemoved(e: ListDataEvent?) {
refreshButton()
}
override fun contentsChanged(e: ListDataEvent?) {
refreshButton()
}
})
return FormBuilder.create().layout(layout).debug(false).padding("0dlu, $FORM_MARGIN, 0dlu, $FORM_MARGIN") return FormBuilder.create().layout(layout).debug(false).padding("0dlu, $FORM_MARGIN, 0dlu, $FORM_MARGIN")
.add("${I18n.getString("termora.settings.account.server")}:").xy(1, rows) .add("${I18n.getString("termora.settings.account.server")}:").xy(1, rows)
@@ -166,6 +184,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
.build() .build()
} }
override fun createOkAction(): AbstractAction { override fun createOkAction(): AbstractAction {
return okAction return okAction
} }
@@ -250,7 +269,12 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
override fun doOKAction() { override fun doOKAction() {
if (isLoggingIn.get()) return if (isLoggingIn.get()) return
val server = serverComboBox.selectedItem as? Server ?: return val server = serverComboBox.selectedItem as? Server
if (server == null) {
serverComboBox.outline = FlatClientProperties.OUTLINE_ERROR
serverComboBox.requestFocusInWindow()
return
}
if (usernameTextField.text.isBlank()) { if (usernameTextField.text.isBlank()) {
usernameTextField.outline = FlatClientProperties.OUTLINE_ERROR usernameTextField.outline = FlatClientProperties.OUTLINE_ERROR

View File

@@ -1,13 +1,12 @@
package app.termora.database package app.termora.database
import app.termora.LocalSecret
import app.termora.randomUUID import app.termora.randomUUID
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.jetbrains.exposed.v1.core.Column import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.Table import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.crypt.Algorithms import org.jetbrains.exposed.v1.crypt.Algorithms
object DataEntity : Table() { internal object DataEntity : Table() {
val id: Column<String> = char("id", length = 32).clientDefault { randomUUID() } val id: Column<String> = char("id", length = 32).clientDefault { randomUUID() }
/** /**
@@ -45,8 +44,8 @@ object DataEntity : Table() {
*/ */
val data: Column<String> = encryptedText( val data: Column<String> = encryptedText(
"data", Algorithms.AES_256_PBE_GCM( "data", Algorithms.AES_256_PBE_GCM(
LocalSecret.getInstance().password, DatabaseSecret.getInstance().password,
LocalSecret.getInstance().salt DatabaseSecret.getInstance().salt
) )
) )

View File

@@ -27,6 +27,10 @@ import kotlin.reflect.KProperty
class DatabaseManager private constructor() : Disposable { class DatabaseManager private constructor() : Disposable {
companion object { companion object {
private const val DB_PASSWORD = "DB_PASSWORD"
private const val DB_SALT = "DB_SALT"
val log = LoggerFactory.getLogger(DatabaseManager::class.java)!! val log = LoggerFactory.getLogger(DatabaseManager::class.java)!!
fun getInstance(): DatabaseManager { fun getInstance(): DatabaseManager {
return ApplicationScope.forApplicationScope() return ApplicationScope.forApplicationScope()
@@ -42,6 +46,15 @@ class DatabaseManager private constructor() : Disposable {
val appearance by lazy { Appearance(this) } val appearance by lazy { Appearance(this) }
val sftp by lazy { SFTP(this) } val sftp by lazy { SFTP(this) }
@Volatile
internal var dbPassword = StringUtils.EMPTY
private set
@Volatile
internal var dbSalt = StringUtils.EMPTY
private set
private val map = Collections.synchronizedMap<String, String?>(mutableMapOf()) private val map = Collections.synchronizedMap<String, String?>(mutableMapOf())
private val accountManager get() = AccountManager.getInstance() private val accountManager get() = AccountManager.getInstance()
@@ -59,6 +72,14 @@ class DatabaseManager private constructor() : Disposable {
driver = "org.sqlite.JDBC", user = "sa" driver = "org.sqlite.JDBC", user = "sa"
) )
// 没有加密的表优先创建
if (isExists.not()) {
transaction(database) { SchemaUtils.create(UnsafeSettingEntity) }
}
// 获取密钥信息
transaction(database) { DatabaseSecret.getInstance(database) }
// 设置数据库版本号,便于后续升级 // 设置数据库版本号,便于后续升级
if (isExists.not()) { if (isExists.not()) {
transaction(database) { transaction(database) {

View File

@@ -0,0 +1,65 @@
package app.termora.database
import app.termora.ApplicationScope
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.lang3.RandomUtils
import org.apache.commons.lang3.StringUtils
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
internal class DatabaseSecret(database: Database) {
companion object {
private const val PASSWORD = "__DB_PASSWORD"
private const val SALT = "__DB_SALT"
fun getInstance(database: Database): DatabaseSecret {
return ApplicationScope.forApplicationScope()
.getOrCreate(DatabaseSecret::class) { DatabaseSecret(database) }
}
fun getInstance(): DatabaseSecret {
return ApplicationScope.forApplicationScope().get(DatabaseSecret::class)
}
}
@Volatile
var password: String = StringUtils.EMPTY
private set
@Volatile
var salt: String = StringUtils.EMPTY
private set
init {
transaction(database) {
val unsafeSettings = UnsafeSettingEntity.selectAll()
.map { it[UnsafeSettingEntity.name] to it[UnsafeSettingEntity.value] }
.associateBy { it.first }
if (unsafeSettings.containsKey(PASSWORD)) {
password = unsafeSettings.getValue(PASSWORD).second
} else {
password =
StringUtils.substring(DigestUtils.sha256Hex(RandomUtils.secureStrong().randomBytes(128)), 0, 16)
UnsafeSettingEntity.insert {
it[UnsafeSettingEntity.name] = PASSWORD
it[UnsafeSettingEntity.value] = password
}
}
if (unsafeSettings.containsKey(SALT)) {
salt = unsafeSettings.getValue(SALT).second
} else {
salt =
StringUtils.substring(DigestUtils.sha256Hex(RandomUtils.secureStrong().randomBytes(128)), 0, 12)
UnsafeSettingEntity.insert {
it[UnsafeSettingEntity.name] = SALT
it[UnsafeSettingEntity.value] = salt
}
}
}
}
}

View File

@@ -1,19 +1,18 @@
package app.termora.database package app.termora.database
import app.termora.LocalSecret
import app.termora.randomUUID import app.termora.randomUUID
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.jetbrains.exposed.v1.core.Column import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.Table import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.crypt.Algorithms import org.jetbrains.exposed.v1.crypt.Algorithms
object SettingEntity : Table() { internal object SettingEntity : Table() {
val id: Column<String> = char("id", length = 32).clientDefault { randomUUID() } val id: Column<String> = char("id", length = 32).clientDefault { randomUUID() }
val name: Column<String> = varchar("name", length = 128).index() val name: Column<String> = varchar("name", length = 128).index()
val value: Column<String> = encryptedText( val value: Column<String> = encryptedText(
"value", Algorithms.AES_256_PBE_GCM( "value", Algorithms.AES_256_PBE_GCM(
LocalSecret.getInstance().password, DatabaseSecret.getInstance().password,
LocalSecret.getInstance().salt DatabaseSecret.getInstance().salt
) )
) )

View File

@@ -0,0 +1,23 @@
package app.termora.database
import app.termora.randomUUID
import org.apache.commons.lang3.StringUtils
import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.Table
internal object UnsafeSettingEntity : Table() {
val id: Column<String> = char("id", length = 32).clientDefault { randomUUID() }
val name: Column<String> = varchar("name", length = 128).index()
val value: Column<String> = text("value")
/**
* 备用字段1-3
*/
val extra1: Column<String> = text("extra1").clientDefault { StringUtils.EMPTY }
val extra2: Column<String> = text("extra2").clientDefault { StringUtils.EMPTY }
val extra3: Column<String> = text("extra3").clientDefault { StringUtils.EMPTY }
override val primaryKey: PrimaryKey get() = PrimaryKey(id)
override val tableName: String
get() = "tb_unsafe_setting"
}