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