Mercurial > cgi-bin > hgweb.cgi > PassMan
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))