Mercurial > cgi-bin > hgweb.cgi > PassMan
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/name/blackcap/passman/UpdateSubcommand.kt Sun Sep 11 16:11:37 2022 -0700 @@ -0,0 +1,150 @@ +package name.blackcap.passman + +import org.apache.commons.cli.CommandLine +import org.apache.commons.cli.DefaultParser +import org.apache.commons.cli.Options +import org.apache.commons.cli.ParseException +import java.sql.Types +import java.util.* +import kotlin.properties.Delegates +import kotlin.system.exitProcess + +class UpdateSubcommand(): Subcommand() { + private lateinit var commandLine: CommandLine + private lateinit var db: Database + private lateinit var nameIn: String + private var id by Delegates.notNull<Long>() + private var length by Delegates.notNull<Int>() + private var generate by Delegates.notNull<Boolean>() + private var allowSymbols by Delegates.notNull<Boolean>() + private var verbose by Delegates.notNull<Boolean>() + private val fields = StringBuilder() + private val fieldValues = mutableListOf<Any?>() + + private companion object { + const val NULL_SPECIFIED = "." + val NULLABLE_FIELDS = setOf<String>("notes") + val SENSITIVE_FIELDS = setOf<String>("password") + } + + override fun run(args: Array<String>) { + parseArguments(args) + checkDatabase() + update() + } + + private fun parseArguments(args: Array<String>) { + val options = Options().apply { + addOption("g", "generate", false, "Use password generator.") + addOption("l", "length", true, "Length of generated password.") + addOption("s", "symbols", false, "Use symbol characters in generated password.") + addOption("v", "verbose", false, "Print the generated password.") + } + try { + commandLine = DefaultParser().parse(options, args) + } catch (e: ParseException) { + die(e.message ?: "syntax error", 2) + } + checkArguments() + db = Database.open() + nameIn = commandLine.args[0] + id = db.makeKey(nameIn) + length = commandLine.getOptionValue("length").let { rawLength -> + try { + rawLength?.toInt() ?: DEFAULT_GENERATED_LENGTH + } catch (e: NumberFormatException) { + die("$rawLength - invalid length") + -1 /* will never happen */ + } + } + generate = commandLine.hasOption("generate") + allowSymbols = commandLine.hasOption("symbols") + verbose = commandLine.hasOption("verbose") + } + + private fun checkArguments(): Unit { + var bad = false + if (!commandLine.hasOption("generate")) { + for (option in listOf<String>("length", "symbols", "verbose")) { + if (commandLine.hasOption(option)) { + error("--$option requires --generate") + bad = true + } + } + } + if (commandLine.args.isEmpty()) { + error("expecting site name") + } + if (commandLine.args.size > 1) { + error("unexpected trailing arguments") + } + if (bad) { + exitProcess(2); + } + } + + private fun checkDatabase(): Unit { + db.connection.prepareStatement("select count(*) from passwords where id = ?").use { + it.setLong(1, id) + val result = it.executeQuery() + result.next() + val count = result.getInt(1) + if (count < 1) { + die("no record matches $nameIn") + } + } + } + + private fun update(): Unit { + updateOne("username") + updateOne("password") + updateOne("notes") + if (fieldValues.isEmpty()) { + error("no values changed") + return + } + + db.connection.prepareStatement("update passwords set updated = ?, $fields where id = ?").use { stmt -> + stmt.setLong(1, System.currentTimeMillis()) + fieldValues.indices.forEach { fieldIndex -> + val fieldValue = fieldValues[fieldIndex] + val columnIndex = fieldIndex + 2 + when (fieldValue) { + is String -> stmt.setEncryptedString(columnIndex, fieldValue, db.encryption) + is CharArray -> stmt.setEncrypted(columnIndex, fieldValue, db.encryption) + null -> stmt.setNull(columnIndex, Types.BLOB) + else -> throw RuntimeException("this shouldn't happen") + } + } + stmt.setLong(fieldValues.size + 2, id) + stmt.execute() + } + } + + private fun updateOne(name: String): Unit { + val prompt = name.replaceFirstChar { it.titlecase(Locale.getDefault()) } + ": " + val value: Any? = if (name in SENSITIVE_FIELDS) { + getPassword(prompt, verify = true) + } else { + val rawValue = readLine(prompt) + if (name in NULLABLE_FIELDS && rawValue == NULL_SPECIFIED) { null } else { rawValue } + } + + val noChange = when (value) { + is String -> value.isEmpty() + is CharArray -> value.isEmpty() + else -> false + } + if (noChange) { + return + } + + if (fields.isNotEmpty()) { + fields.append(", ") + } + fields.append(name) + fields.append(" = ?") + fieldValues.add(value) + } + +} \ No newline at end of file