comparison 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
comparison
equal deleted inserted replaced
-1:000000000000 0:a6cfdffcaa94
1 package name.blackcap.passman
2
3 import java.nio.file.Files
4 import java.nio.file.Path
5 import java.security.GeneralSecurityException
6 import java.security.SecureRandom
7 import java.sql.*
8
9 class Database private constructor(val connection: Connection, val encryption: Encryption){
10
11 companion object {
12 private const val PLAINTEXT = "This is a test."
13 private const val SALT_LENGTH = 16
14 private const val DEFAULT_PROMPT = "Decryption key: "
15
16 fun open(passwordPrompt: String = DEFAULT_PROMPT, fileName: String = DB_FILE,
17 create: Boolean = true): Database {
18 val exists = Files.exists(Path.of(fileName))
19 if (!exists) {
20 if (create) {
21 error("initializing database $fileName")
22 } else {
23 die("$fileName not found")
24 }
25 }
26 val masterPassword = getPassword(passwordPrompt, !exists)
27 val conn = DriverManager.getConnection("jdbc:sqlite:$fileName")
28 val enc = if (exists) { reuse(conn, masterPassword) } else { init(conn, masterPassword) }
29 val ret = Database(conn, enc)
30 verifyPassword(ret)
31 return ret
32 }
33
34 private fun reuse(connection: Connection, masterPassword: CharArray): Encryption {
35 try {
36 connection.prepareStatement("select value from blobs where name = ?").use {
37 it.setString(1, "salt")
38 val result = it.executeQuery()
39 if (!result.next()) {
40 die("corrupt database, missing salt parameter")
41 }
42 val salt = result.getBytes(1)
43 return Encryption(masterPassword, salt)
44 }
45 } catch (e: SQLException) {
46 e.printStackTrace()
47 die("unable to reopen database")
48 throw RuntimeException("this will never happen")
49 }
50 }
51
52 private fun init(connection: Connection, masterPassword: CharArray): Encryption {
53 try {
54 connection.createStatement().use { stmt ->
55 stmt.executeUpdate("create table integers ( name string not null, value integer )")
56 stmt.executeUpdate("create table reals ( name string not null, value integer )")
57 stmt.executeUpdate("create table strings ( name string not null, value real )")
58 stmt.executeUpdate("create table blobs ( name string not null, value blob )")
59 stmt.executeUpdate(
60 "create table passwords (" +
61 "id integer not null primary key, " +
62 "name blob not null, " +
63 "username blob not null, " +
64 "password blob not null, " +
65 "notes blob, " +
66 "created integer, " +
67 "modified integer, " +
68 "accessed integer )"
69 )
70 }
71 val salt = ByteArray(SALT_LENGTH).also { SecureRandom().nextBytes(it) }
72 val encryption = Encryption(masterPassword, salt)
73 connection.prepareStatement("insert into blobs (name, value) values (?, ?)").use {
74 it.setString(1, "salt")
75 it.setBytes(2, salt)
76 it.execute()
77 }
78 connection.prepareStatement("insert into blobs (name, value) values (?, ?)").use { stmt ->
79 stmt.setString(1, "test")
80 stmt.setEncryptedString(2, PLAINTEXT, encryption)
81 stmt.execute()
82 }
83 return encryption
84 } catch (e: SQLException) {
85 e.printStackTrace()
86 die("unable to initialize database")
87 throw RuntimeException("this will never happen")
88 }
89 }
90
91 private fun verifyPassword(database: Database) {
92 try {
93 database.connection.prepareStatement("select value from blobs where name = ?").use { stmt ->
94 stmt.setString(1, "test")
95 val result = stmt.executeQuery()
96 if (!result.next()) {
97 die("corrupt database, missing test parameter")
98 }
99 val readFromDb = result.getDecryptedString(1, database.encryption)
100 if (readFromDb != PLAINTEXT) {
101 /* might also get thrown by getDecryptedString if bad */
102 println(" got: " + dump(readFromDb))
103 println("expected: " + dump(PLAINTEXT))
104 throw GeneralSecurityException("bad key!")
105 }
106 }
107 } catch (e: SQLException) {
108 e.printStackTrace()
109 die("unable to verify decryption key")
110 } catch (e: GeneralSecurityException) {
111 die("invalid decryption key")
112 }
113 }
114 }
115
116 fun makeKey(name: String): Long = Hashing.hash(encryption.encryptFromString0(name.lowercase()))
117 }
118
119 public fun ResultSet.getDecryptedString(columnIndex: Int, encryption: Encryption) =
120 encryption.decryptToString(getBytes(columnIndex))
121
122 public fun ResultSet.getDecrypted(columnIndex: Int, encryption: Encryption) =
123 encryption.decrypt(getBytes(columnIndex))
124
125 public fun ResultSet.getDate(columnIndex: Int): java.util.Date? {
126 val rawDate = getLong(columnIndex)
127 return if (wasNull()) { null } else { java.util.Date(rawDate) }
128 }
129
130 public fun PreparedStatement.setEncryptedString(columnIndex: Int, value: String, encryption: Encryption) =
131 setBytes(columnIndex, encryption.encryptFromString(value))
132
133 public fun PreparedStatement.setEncrypted(columnIndex: Int, value: CharArray, encryption: Encryption) =
134 setBytes(columnIndex, encryption.encrypt(value))