Mercurial > cgi-bin > hgweb.cgi > PassMan
view src/main/kotlin/name/blackcap/passman/Database.kt @ 3:eafa3779aef8
More bug fixes, quote strings in diagnostics.
author | David Barts <n5jrn@me.com> |
---|---|
date | Sun, 11 Sep 2022 20:36:06 -0700 |
parents | a6cfdffcaa94 |
children | ad997df1f560 |
line wrap: on
line source
package name.blackcap.passman import java.nio.file.Files import java.nio.file.Path import java.security.GeneralSecurityException import java.security.SecureRandom import java.sql.* class Database private constructor(val connection: Connection, val encryption: Encryption){ companion object { private const val PLAINTEXT = "This is a test." private const val SALT_LENGTH = 16 private const val DEFAULT_PROMPT = "Decryption key: " fun open(passwordPrompt: String = DEFAULT_PROMPT, fileName: String = DB_FILE, create: Boolean = true): Database { val exists = Files.exists(Path.of(fileName)) if (!exists) { if (create) { error("initializing database ${see(fileName, simple = true)}") } else { die("${see(fileName, simple = true)} not found") } } val masterPassword = getPassword(passwordPrompt, !exists) val conn = DriverManager.getConnection("jdbc:sqlite:$fileName") val enc = if (exists) { reuse(conn, masterPassword) } else { init(conn, masterPassword) } val ret = Database(conn, enc) verifyPassword(ret) return ret } private fun reuse(connection: Connection, masterPassword: CharArray): Encryption { try { connection.prepareStatement("select value from blobs where name = ?").use { it.setString(1, "salt") val result = it.executeQuery() if (!result.next()) { die("corrupt database, missing salt parameter") } val salt = result.getBytes(1) return Encryption(masterPassword, salt) } } catch (e: SQLException) { e.printStackTrace() die("unable to reopen database") throw RuntimeException("this will never happen") } } private fun init(connection: Connection, masterPassword: CharArray): Encryption { try { connection.createStatement().use { stmt -> stmt.executeUpdate("create table integers ( name string not null, value integer )") stmt.executeUpdate("create table reals ( name string not null, value integer )") stmt.executeUpdate("create table strings ( name string not null, value real )") stmt.executeUpdate("create table blobs ( name string not null, value blob )") stmt.executeUpdate( "create table passwords (" + "id integer not null primary key, " + "name blob not null, " + "username blob not null, " + "password blob not null, " + "notes blob, " + "created integer, " + "modified integer, " + "accessed integer )" ) } val salt = ByteArray(SALT_LENGTH).also { SecureRandom().nextBytes(it) } val encryption = Encryption(masterPassword, salt) connection.prepareStatement("insert into blobs (name, value) values (?, ?)").use { it.setString(1, "salt") it.setBytes(2, salt) it.execute() } connection.prepareStatement("insert into blobs (name, value) values (?, ?)").use { stmt -> stmt.setString(1, "test") stmt.setEncryptedString(2, PLAINTEXT, encryption) stmt.execute() } return encryption } catch (e: SQLException) { e.printStackTrace() die("unable to initialize database") throw RuntimeException("this will never happen") } } private fun verifyPassword(database: Database) { try { database.connection.prepareStatement("select value from blobs where name = ?").use { stmt -> stmt.setString(1, "test") val result = stmt.executeQuery() if (!result.next()) { die("corrupt database, missing test parameter") } val readFromDb = result.getDecryptedString(1, database.encryption) if (readFromDb != PLAINTEXT) { /* might also get thrown by getDecryptedString if bad */ throw GeneralSecurityException("bad key!") } } } catch (e: SQLException) { e.printStackTrace() die("unable to verify decryption key") } catch (e: GeneralSecurityException) { die("invalid decryption key") } } } fun makeKey(name: String): Long = Hashing.hash(encryption.encryptFromString0(name.lowercase())) } public fun ResultSet.getDecryptedString(columnIndex: Int, encryption: Encryption) = encryption.decryptToString(getBytes(columnIndex)) public fun ResultSet.getDecrypted(columnIndex: Int, encryption: Encryption) = encryption.decrypt(getBytes(columnIndex)) public fun ResultSet.getDate(columnIndex: Int): java.util.Date? { val rawDate = getLong(columnIndex) return if (wasNull()) { null } else { java.util.Date(rawDate) } } public fun PreparedStatement.setEncryptedString(columnIndex: Int, value: String, encryption: Encryption) = setBytes(columnIndex, encryption.encryptFromString(value)) public fun PreparedStatement.setEncrypted(columnIndex: Int, value: CharArray, encryption: Encryption) = setBytes(columnIndex, encryption.encrypt(value))