diff src/main/kotlin/name/blackcap/passman/Database.kt @ 0:a6cfdffcaa94

Initial commit, incomplete but it runs sorta.
author David Barts <n5jrn@me.com>
date Sun, 11 Sep 2022 16:11:37 -0700
parents
children eafa3779aef8
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/name/blackcap/passman/Database.kt	Sun Sep 11 16:11:37 2022 -0700
@@ -0,0 +1,134 @@
+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 $fileName")
+                } else {
+                    die("$fileName 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 */
+                        println("     got: " + dump(readFromDb))
+                        println("expected: " + dump(PLAINTEXT))
+                        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))