Mercurial > cgi-bin > hgweb.cgi > PassMan
comparison src/main/kotlin/name/blackcap/passman/UpdateSubcommand.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 | 3c792ad36b3d |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:a6cfdffcaa94 |
---|---|
1 package name.blackcap.passman | |
2 | |
3 import org.apache.commons.cli.CommandLine | |
4 import org.apache.commons.cli.DefaultParser | |
5 import org.apache.commons.cli.Options | |
6 import org.apache.commons.cli.ParseException | |
7 import java.sql.Types | |
8 import java.util.* | |
9 import kotlin.properties.Delegates | |
10 import kotlin.system.exitProcess | |
11 | |
12 class UpdateSubcommand(): Subcommand() { | |
13 private lateinit var commandLine: CommandLine | |
14 private lateinit var db: Database | |
15 private lateinit var nameIn: String | |
16 private var id by Delegates.notNull<Long>() | |
17 private var length by Delegates.notNull<Int>() | |
18 private var generate by Delegates.notNull<Boolean>() | |
19 private var allowSymbols by Delegates.notNull<Boolean>() | |
20 private var verbose by Delegates.notNull<Boolean>() | |
21 private val fields = StringBuilder() | |
22 private val fieldValues = mutableListOf<Any?>() | |
23 | |
24 private companion object { | |
25 const val NULL_SPECIFIED = "." | |
26 val NULLABLE_FIELDS = setOf<String>("notes") | |
27 val SENSITIVE_FIELDS = setOf<String>("password") | |
28 } | |
29 | |
30 override fun run(args: Array<String>) { | |
31 parseArguments(args) | |
32 checkDatabase() | |
33 update() | |
34 } | |
35 | |
36 private fun parseArguments(args: Array<String>) { | |
37 val options = Options().apply { | |
38 addOption("g", "generate", false, "Use password generator.") | |
39 addOption("l", "length", true, "Length of generated password.") | |
40 addOption("s", "symbols", false, "Use symbol characters in generated password.") | |
41 addOption("v", "verbose", false, "Print the generated password.") | |
42 } | |
43 try { | |
44 commandLine = DefaultParser().parse(options, args) | |
45 } catch (e: ParseException) { | |
46 die(e.message ?: "syntax error", 2) | |
47 } | |
48 checkArguments() | |
49 db = Database.open() | |
50 nameIn = commandLine.args[0] | |
51 id = db.makeKey(nameIn) | |
52 length = commandLine.getOptionValue("length").let { rawLength -> | |
53 try { | |
54 rawLength?.toInt() ?: DEFAULT_GENERATED_LENGTH | |
55 } catch (e: NumberFormatException) { | |
56 die("$rawLength - invalid length") | |
57 -1 /* will never happen */ | |
58 } | |
59 } | |
60 generate = commandLine.hasOption("generate") | |
61 allowSymbols = commandLine.hasOption("symbols") | |
62 verbose = commandLine.hasOption("verbose") | |
63 } | |
64 | |
65 private fun checkArguments(): Unit { | |
66 var bad = false | |
67 if (!commandLine.hasOption("generate")) { | |
68 for (option in listOf<String>("length", "symbols", "verbose")) { | |
69 if (commandLine.hasOption(option)) { | |
70 error("--$option requires --generate") | |
71 bad = true | |
72 } | |
73 } | |
74 } | |
75 if (commandLine.args.isEmpty()) { | |
76 error("expecting site name") | |
77 } | |
78 if (commandLine.args.size > 1) { | |
79 error("unexpected trailing arguments") | |
80 } | |
81 if (bad) { | |
82 exitProcess(2); | |
83 } | |
84 } | |
85 | |
86 private fun checkDatabase(): Unit { | |
87 db.connection.prepareStatement("select count(*) from passwords where id = ?").use { | |
88 it.setLong(1, id) | |
89 val result = it.executeQuery() | |
90 result.next() | |
91 val count = result.getInt(1) | |
92 if (count < 1) { | |
93 die("no record matches $nameIn") | |
94 } | |
95 } | |
96 } | |
97 | |
98 private fun update(): Unit { | |
99 updateOne("username") | |
100 updateOne("password") | |
101 updateOne("notes") | |
102 if (fieldValues.isEmpty()) { | |
103 error("no values changed") | |
104 return | |
105 } | |
106 | |
107 db.connection.prepareStatement("update passwords set updated = ?, $fields where id = ?").use { stmt -> | |
108 stmt.setLong(1, System.currentTimeMillis()) | |
109 fieldValues.indices.forEach { fieldIndex -> | |
110 val fieldValue = fieldValues[fieldIndex] | |
111 val columnIndex = fieldIndex + 2 | |
112 when (fieldValue) { | |
113 is String -> stmt.setEncryptedString(columnIndex, fieldValue, db.encryption) | |
114 is CharArray -> stmt.setEncrypted(columnIndex, fieldValue, db.encryption) | |
115 null -> stmt.setNull(columnIndex, Types.BLOB) | |
116 else -> throw RuntimeException("this shouldn't happen") | |
117 } | |
118 } | |
119 stmt.setLong(fieldValues.size + 2, id) | |
120 stmt.execute() | |
121 } | |
122 } | |
123 | |
124 private fun updateOne(name: String): Unit { | |
125 val prompt = name.replaceFirstChar { it.titlecase(Locale.getDefault()) } + ": " | |
126 val value: Any? = if (name in SENSITIVE_FIELDS) { | |
127 getPassword(prompt, verify = true) | |
128 } else { | |
129 val rawValue = readLine(prompt) | |
130 if (name in NULLABLE_FIELDS && rawValue == NULL_SPECIFIED) { null } else { rawValue } | |
131 } | |
132 | |
133 val noChange = when (value) { | |
134 is String -> value.isEmpty() | |
135 is CharArray -> value.isEmpty() | |
136 else -> false | |
137 } | |
138 if (noChange) { | |
139 return | |
140 } | |
141 | |
142 if (fields.isNotEmpty()) { | |
143 fields.append(", ") | |
144 } | |
145 fields.append(name) | |
146 fields.append(" = ?") | |
147 fieldValues.add(value) | |
148 } | |
149 | |
150 } |