Mercurial > cgi-bin > hgweb.cgi > PassMan
annotate src/main/kotlin/name/blackcap/passman/Database.kt @ 27:3a3067ba673b
Add idle-time detection to interactive mode, clean up imports.
author | David Barts <n5jrn@me.com> |
---|---|
date | Sat, 27 Jul 2024 09:50:54 -0700 |
parents | ea65ab890f66 |
children |
rev | line source |
---|---|
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
1 package name.blackcap.passman |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
2 |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
3 import java.nio.file.Files |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
4 import java.nio.file.Path |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
5 import java.security.GeneralSecurityException |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
6 import java.security.SecureRandom |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
7 import java.sql.* |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
8 |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
9 class Database private constructor(val connection: Connection, val encryption: Encryption){ |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
10 |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
11 companion object { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
12 private const val PLAINTEXT = "This is a test." |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
13 private const val SALT_LENGTH = 16 |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
14 private const val DEFAULT_PROMPT = "Decryption key: " |
21
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
15 lateinit var default: Database |
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
16 |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
17 fun open(passwordPrompt: String = DEFAULT_PROMPT, fileName: String = DB_FILE, |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
18 create: Boolean = true): Database { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
19 val exists = Files.exists(Path.of(fileName)) |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
20 if (!exists) { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
21 if (create) { |
5
ad997df1f560
Fix see() to be about as good as sccc.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
22 error("initializing database ${see(fileName)}") |
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
23 } else { |
21
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
24 throw DatabaseException("${see(fileName)} not found") |
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
25 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
26 } |
21
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
27 val masterPassword = try { |
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
28 getPassword(passwordPrompt, !exists) |
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
29 } catch (e: ConsoleException) { |
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
30 throw DatabaseException(e.message, cause = e) |
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
31 } |
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
32 val conn = DriverManager.getConnection("jdbc:sqlite:$fileName") |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
33 val enc = if (exists) { reuse(conn, masterPassword) } else { init(conn, masterPassword) } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
34 val ret = Database(conn, enc) |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
35 verifyPassword(ret) |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
36 return ret |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
37 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
38 |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
39 private fun reuse(connection: Connection, masterPassword: CharArray): Encryption { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
40 try { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
41 connection.prepareStatement("select value from blobs where name = ?").use { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
42 it.setString(1, "salt") |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
43 val result = it.executeQuery() |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
44 if (!result.next()) { |
21
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
45 throw DatabaseException("corrupt database, missing salt parameter") |
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
46 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
47 val salt = result.getBytes(1) |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
48 return Encryption(masterPassword, salt) |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
49 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
50 } catch (e: SQLException) { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
51 e.printStackTrace() |
21
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
52 throw DatabaseException("unable to reopen database", e) |
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
53 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
54 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
55 |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
56 private fun init(connection: Connection, masterPassword: CharArray): Encryption { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
57 try { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
58 connection.createStatement().use { stmt -> |
19
7d80cbcb67bb
add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
16
diff
changeset
|
59 stmt.executeUpdate("create table integers ( name text not null, value integer )") |
7d80cbcb67bb
add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
16
diff
changeset
|
60 stmt.executeUpdate("create table reals ( name text not null, value real )") |
7d80cbcb67bb
add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
16
diff
changeset
|
61 stmt.executeUpdate("create table strings ( name text not null, value text )") |
7d80cbcb67bb
add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
16
diff
changeset
|
62 stmt.executeUpdate("create table blobs ( name text not null, value blob )") |
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
63 stmt.executeUpdate( |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
64 "create table passwords (" + |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
65 "id integer not null primary key, " + |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
66 "name blob not null, " + |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
67 "username blob not null, " + |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
68 "password blob not null, " + |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
69 "notes blob, " + |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
70 "created integer, " + |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
71 "modified integer, " + |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
72 "accessed integer )" |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
73 ) |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
74 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
75 val salt = ByteArray(SALT_LENGTH).also { SecureRandom().nextBytes(it) } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
76 val encryption = Encryption(masterPassword, salt) |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
77 connection.prepareStatement("insert into blobs (name, value) values (?, ?)").use { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
78 it.setString(1, "salt") |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
79 it.setBytes(2, salt) |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
80 it.execute() |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
81 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
82 connection.prepareStatement("insert into blobs (name, value) values (?, ?)").use { stmt -> |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
83 stmt.setString(1, "test") |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
84 stmt.setEncryptedString(2, PLAINTEXT, encryption) |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
85 stmt.execute() |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
86 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
87 return encryption |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
88 } catch (e: SQLException) { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
89 e.printStackTrace() |
21
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
90 throw DatabaseException("unable to initialize database", e) |
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
91 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
92 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
93 |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
94 private fun verifyPassword(database: Database) { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
95 try { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
96 database.connection.prepareStatement("select value from blobs where name = ?").use { stmt -> |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
97 stmt.setString(1, "test") |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
98 val result = stmt.executeQuery() |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
99 if (!result.next()) { |
21
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
100 throw DatabaseException("corrupt database, missing test parameter") |
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
101 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
102 val readFromDb = result.getDecryptedString(1, database.encryption) |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
103 if (readFromDb != PLAINTEXT) { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
104 /* might also get thrown by getDecryptedString if bad */ |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
105 throw GeneralSecurityException("bad key!") |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
106 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
107 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
108 } catch (e: SQLException) { |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
109 e.printStackTrace() |
21
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
110 throw DatabaseException("unable to verify decryption key", e) |
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
111 } catch (e: GeneralSecurityException) { |
21
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
112 throw DatabaseException("invalid decryption key", e) |
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
113 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
114 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
115 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
116 |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
117 fun makeKey(name: String): Long = Hashing.hash(encryption.encryptFromString0(name.lowercase())) |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
118 } |
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
119 |
21
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
120 class DatabaseException(message: String, cause: Throwable? = null) : MessagedException(message, cause) |
ea65ab890f66
More work to support interactive feature.
David Barts <n5jrn@me.com>
parents:
19
diff
changeset
|
121 |
11 | 122 fun ResultSet.getDecryptedString(columnIndex: Int, encryption: Encryption): String? { |
8 | 123 return encryption.decryptToString(getBytes(columnIndex) ?: return null) |
124 } | |
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
125 |
11 | 126 fun ResultSet.getDecrypted(columnIndex: Int, encryption: Encryption): CharArray? { |
8 | 127 return encryption.decrypt(getBytes(columnIndex) ?: return null) |
128 } | |
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
129 |
11 | 130 fun PreparedStatement.setEncryptedString(columnIndex: Int, value: String?, encryption: Encryption) = |
8 | 131 if (value == null) { |
132 setNull(columnIndex, Types.BLOB) | |
133 } else { | |
134 setBytes(columnIndex, encryption.encryptFromString(value)) | |
135 } | |
0
a6cfdffcaa94
Initial commit, incomplete but it runs sorta.
David Barts <n5jrn@me.com>
parents:
diff
changeset
|
136 |
11 | 137 fun PreparedStatement.setEncrypted(columnIndex: Int, value: CharArray?, encryption: Encryption) = |
8 | 138 if (value == null) { |
139 setNull(columnIndex, Types.BLOB) | |
140 } else { | |
141 setBytes(columnIndex, encryption.encrypt(value)) | |
142 } | |
11 | 143 |
144 fun PreparedStatement.setBytesOrNull(columnIndex: Int, value: ByteArray?) = | |
145 if (value == null) { | |
146 setNull(columnIndex, Types.BLOB) | |
147 } else { | |
148 setBytes(columnIndex, value) | |
149 } | |
150 | |
151 fun PreparedStatement.setLongOrNull(columnIndex: Int, value: Long?) = | |
152 if (value == null) { | |
153 setNull(columnIndex, Types.INTEGER) | |
154 } else { | |
155 setLong(columnIndex, value) | |
156 } | |
15 | 157 |
158 fun PreparedStatement.setDateOrNull(parameterIndex: Int, value: Long?) { | |
159 if (value == null || value == 0L) { | |
160 setNull(parameterIndex, Types.INTEGER) | |
161 } else { | |
162 setLong(parameterIndex, value) | |
163 } | |
164 } | |
16 | 165 |