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