changeset 15:0fc90892a3ae

Add password subcommand.
author David Barts <n5jrn@me.com>
date Fri, 03 Feb 2023 18:48:13 -0800
parents 4dae7a15ee48
children 7a74ae668665
files src/main/kotlin/name/blackcap/passman/Database.kt src/main/kotlin/name/blackcap/passman/Files.kt src/main/kotlin/name/blackcap/passman/HelpSubcommand.kt src/main/kotlin/name/blackcap/passman/PasswordSubcommand.kt src/main/kotlin/name/blackcap/passman/RenameSubcommand.kt
diffstat 5 files changed, 94 insertions(+), 27 deletions(-) [+]
line wrap: on
line diff
--- a/src/main/kotlin/name/blackcap/passman/Database.kt	Tue Jan 31 19:07:46 2023 -0800
+++ b/src/main/kotlin/name/blackcap/passman/Database.kt	Fri Feb 03 18:48:13 2023 -0800
@@ -149,3 +149,11 @@
     } else {
         setLong(columnIndex, value)
     }
+
+fun PreparedStatement.setDateOrNull(parameterIndex: Int, value: Long?) {
+    if (value == null || value == 0L) {
+        setNull(parameterIndex, Types.INTEGER)
+    } else {
+        setLong(parameterIndex, value)
+    }
+}
--- a/src/main/kotlin/name/blackcap/passman/Files.kt	Tue Jan 31 19:07:46 2023 -0800
+++ b/src/main/kotlin/name/blackcap/passman/Files.kt	Fri Feb 03 18:48:13 2023 -0800
@@ -1,10 +1,8 @@
 package name.blackcap.passman
 
-import java.io.BufferedReader
-import java.io.File
-import java.io.FileInputStream
-import java.io.InputStreamReader
 import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.nio.file.Path
 import java.util.*
 import kotlin.system.exitProcess
 
@@ -28,27 +26,26 @@
     }
 }
 
-/* joins path name components to java.io.File */
-
-fun joinPath(base: String, vararg rest: String) = rest.fold(File(base), ::File)
-
 /* file names */
 
 const val SHORTNAME = "passman"
 const val MAIN_PACKAGE = "name.blackcap." + SHORTNAME
 private val HOME = System.getenv("HOME")
 private val PF_DIR = when (OS.type) {
-    OS.MAC -> joinPath(HOME, "Library", "Application Support", MAIN_PACKAGE)
-    OS.WINDOWS -> joinPath(System.getenv("APPDATA"), MAIN_PACKAGE)
-    else -> joinPath(HOME, "." + SHORTNAME)
+    OS.MAC -> Path.of(HOME, "Library", "Application Support", MAIN_PACKAGE)
+    OS.WINDOWS -> Path.of(System.getenv("APPDATA"), MAIN_PACKAGE)
+    else -> Path.of(HOME, "." + SHORTNAME)
 }
 
-val PROP_FILE = File(PF_DIR, SHORTNAME + ".properties")
-val DB_FILE: String = File(PF_DIR, SHORTNAME + ".db").absolutePath
+val PROP_FILE = PF_DIR.resolve( SHORTNAME + ".properties")
+val DB_FILE: String = PF_DIR.resolve(SHORTNAME + ".db").toAbsolutePath().toString()
+val NEW_DB_FILE: String = PF_DIR.resolve("new.db").toAbsolutePath().toString()
 
-/* make some needed directories */
+/* make some needed directories/files */
 
-private fun File.makeIfNeeded() = if (exists()) { true } else { mkdirs() }
+private fun makeIfNeeded(p: Path) = if (Files.exists(p)) { p } else { Files.createDirectories(p) }
+
+private fun createIfNeeded(p: Path) = if (Files.exists(p)) { p } else { Files.createFile(p) }
 
 /* make some usable objects */
 
@@ -57,9 +54,9 @@
 }
 
 val PROPERTIES = Properties(DPROPERTIES).apply {
-    PF_DIR.makeIfNeeded()
-    PROP_FILE.createNewFile()
-    BufferedReader(InputStreamReader(FileInputStream(PROP_FILE), StandardCharsets.UTF_8)).use  {
+    makeIfNeeded(PF_DIR)
+    createIfNeeded(PROP_FILE)
+    Files.newBufferedReader(PROP_FILE, StandardCharsets.UTF_8).use  {
         load(it)
     }
 }
--- a/src/main/kotlin/name/blackcap/passman/HelpSubcommand.kt	Tue Jan 31 19:07:46 2023 -0800
+++ b/src/main/kotlin/name/blackcap/passman/HelpSubcommand.kt	Fri Feb 03 18:48:13 2023 -0800
@@ -10,10 +10,11 @@
         println("import       Import from CSV file.")
         println("list         List records.")
         println("merge        Merge passwords in from another PassMan database.")
+        println("password     Change database encryption key.")
         println("read         Retrieve data from existing record.")
         println("rename       Rename existing record.")
         println("update       Update existing record.")
         println()
-        println("Database is $DB_FILE .")
+        println("Database is ${see(DB_FILE)} .")
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/name/blackcap/passman/PasswordSubcommand.kt	Fri Feb 03 18:48:13 2023 -0800
@@ -0,0 +1,69 @@
+package name.blackcap.passman
+
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.StandardCopyOption
+import kotlin.system.exitProcess
+
+class PasswordSubcommand : Subcommand() {
+    override fun run(args: Array<String>) {
+        // Parse arguments
+        if (args.size > 0 && (args[0] == "-h" || args[0].startsWith("--h"))) {
+            println("usage: passman password")
+            exitProcess(0)
+        }
+        if (!args.isEmpty()) {
+            die("unexpected arguments", 2)
+        }
+
+        // Open databases
+        println("Changing database encryption key...")
+        val oldPath = Path.of(DB_FILE)
+        val oldDb = Database.open(fileName = DB_FILE, passwordPrompt = "Old database key: ")
+        val newPath = Path.of(NEW_DB_FILE)
+        if (Files.exists(newPath)) {
+            println("WARNING: deleting ${see(NEW_DB_FILE)}")
+            Files.delete(newPath)
+        }
+        val newDb = Database.open(fileName = NEW_DB_FILE, passwordPrompt = "New database key: ", create = true)
+
+        // Copy old to new
+        println("WARNING: do not interrupt this process or data may be lost!")
+        copyRecords(oldDb, newDb)
+
+        // Wrap up
+        oldDb.connection.close()
+        newDb.connection.close()
+        Files.move(newPath, oldPath, StandardCopyOption.REPLACE_EXISTING)
+        println("Database key changed.")
+    }
+
+    private fun copyRecords(oldDb: Database, newDb: Database) {
+        oldDb.connection.prepareStatement("select name, username, password, notes, created, modified, accessed from passwords").use { old ->
+            val results = old.executeQuery()
+            while (results.next()) {
+                newDb.connection.prepareStatement("insert into passwords (id, name, username, password, notes, created, modified, accessed) values (?, ?, ?, ?, ?, ?, ?, ?)").use { new ->
+                    // id and name
+                    val name = results.getDecryptedString(1, oldDb.encryption)!!
+                    new.setLong(1, newDb.makeKey(name))
+                    new.setEncryptedString(2, name, newDb.encryption)
+                    // username
+                    new.setEncryptedString(3,
+                        results.getDecryptedString(2, oldDb.encryption)!!, newDb.encryption)
+                    // password
+                    val password = results.getDecrypted(3, oldDb.encryption)!!
+                    new.setEncrypted(4, password, newDb.encryption)
+                    password.clear()
+                    // notes
+                    new.setEncryptedString(5,
+                        results.getDecryptedString(4, oldDb.encryption), newDb.encryption)
+                    // created, modified, accessed
+                    new.setDateOrNull(6, results.getLong(5))
+                    new.setDateOrNull(7, results.getLong(6))
+                    new.setDateOrNull(8, results.getLong(7))
+                    new.executeUpdate()
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
--- a/src/main/kotlin/name/blackcap/passman/RenameSubcommand.kt	Tue Jan 31 19:07:46 2023 -0800
+++ b/src/main/kotlin/name/blackcap/passman/RenameSubcommand.kt	Fri Feb 03 18:48:13 2023 -0800
@@ -95,12 +95,4 @@
             it.executeUpdate()
         }
     }
-
-    private fun PreparedStatement.setDateOrNull(parameterIndex: Int, value: Long?) {
-        if (value == null || value == 0L) {
-            setNull(parameterIndex, Types.INTEGER)
-        } else {
-            setLong(parameterIndex, value)
-        }
-    }
 }