Skip to content

Commit

Permalink
create, read, update: handle created & modified property for passwords
Browse files Browse the repository at this point in the history
The modified flag is handy when you want to make sure the password on a
given service is new enough (passwords before a certain time are
considered unsafe). And if we are at it, also add a creation time, too.

Store the date/time in a format like 2024-10-12T20:03:46+02:00, which is
machine-readable, but still more (human-)readable than just git-style
epoch seconds + timezone info.
  • Loading branch information
vmiklos committed Oct 12, 2024
1 parent f1c1955 commit 3337186
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 19 deletions.
4 changes: 4 additions & 0 deletions commands/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package commands
import (
"os"
"os/exec"
"time"

"github.com/mdp/qrterminal/v3"
"github.com/pquerna/otp/totp"
Expand Down Expand Up @@ -36,3 +37,6 @@ var OpenDatabase = openDatabase

// CloseDatabase opens the database before running a subcommand.
var CloseDatabase = closeDatabase

// Now returns the current local time.
var Now = time.Now
6 changes: 4 additions & 2 deletions commands/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"bufio"
"fmt"
"strings"
"time"

"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -40,12 +41,13 @@ func createPassword(context *Context, machine, service, user, password string, p
}

defer transaction.Rollback()
query, err := transaction.Prepare("insert into passwords (machine, service, user, password, type) values(?, ?, ?, ?, ?)")
query, err := transaction.Prepare("insert into passwords (machine, service, user, password, type, created, modified) values(?, ?, ?, ?, ?, ?, ?)")
if err != nil {
return "", fmt.Errorf("db.Prepare() failed: %s", err)
}

result, err := query.Exec(machine, service, user, password, passwordType)
now := Now().Format(time.RFC3339)
result, err := query.Exec(machine, service, user, password, passwordType, now, now)
if err != nil {
return "", fmt.Errorf("query.Exec() failed: %s", err)
}
Expand Down
20 changes: 18 additions & 2 deletions commands/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func readPasswords(db *sql.DB, opts searchOptions) ([]string, error) {
if opts.totp {
opts.wantedType = "totp"
}
rows, err := db.Query("select id, machine, service, user, password, type, archived from passwords")
rows, err := db.Query("select id, machine, service, user, password, type, archived, created, modified from passwords")
if err != nil {
return nil, fmt.Errorf("db.Query(select) failed: %s", err)
}
Expand All @@ -75,7 +75,9 @@ func readPasswords(db *sql.DB, opts searchOptions) ([]string, error) {
var password string
var passwordType PasswordType
var archived bool
err = rows.Scan(&id, &machine, &service, &user, &password, &passwordType, &archived)
var created string
var modified string
err = rows.Scan(&id, &machine, &service, &user, &password, &passwordType, &archived, &created, &modified)
if err != nil {
return nil, fmt.Errorf("rows.Scan() failed: %s", err)
}
Expand Down Expand Up @@ -146,6 +148,20 @@ func readPasswords(db *sql.DB, opts searchOptions) ([]string, error) {
}
if opts.verbose {
result += fmt.Sprintf(", archived: %v", archived)
if len(created) > 0 {
t, err := time.Parse(time.RFC3339, created)
if err != nil {
return nil, fmt.Errorf("time.Parse() failed: %s", err)
}
result += fmt.Sprintf(", created: %v", t.Format("2006-01-02 15:04"))
}
if len(modified) > 0 {
t, err := time.Parse(time.RFC3339, modified)
if err != nil {
return nil, fmt.Errorf("time.Parse() failed: %s", err)
}
result += fmt.Sprintf(", modified: %v", t.Format("2006-01-02 15:04"))
}
}
}
results = append(results, result)
Expand Down
24 changes: 23 additions & 1 deletion commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,30 @@ func initDatabase(ctx *Context) error {
ctx.DatabaseMigrated = true
}

if version < 3 {
query, err := ctx.Database.Prepare(`alter table passwords add column
created text not null default ''`)
if err != nil {
return fmt.Errorf("db.Prepare() failed: %s", err)
}
_, err = query.Exec()
if err != nil {
return fmt.Errorf("db.Exec() failed: %s", err)
}
query, err = ctx.Database.Prepare(`alter table passwords add column
modified text not null default ''`)
if err != nil {
return fmt.Errorf("db.Prepare() failed: %s", err)
}
_, err = query.Exec()
if err != nil {
return fmt.Errorf("db.Exec() failed: %s", err)
}
ctx.DatabaseMigrated = true
}

if ctx.DatabaseMigrated {
query, err := ctx.Database.Prepare("pragma user_version = 2")
query, err := ctx.Database.Prepare("pragma user_version = 3")
if err != nil {
return fmt.Errorf("db.Prepare() failed: %s", err)
}
Expand Down
4 changes: 4 additions & 0 deletions commands/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ func CreateContextForTesting(t *testing.T) Context {
GenerateTotpCode = GenerateTotpCodeForTesting
t.Cleanup(func() { GenerateTotpCode = oldGenerateTotpCode })

oldNow := Now
Now = NowForTesting
t.Cleanup(func() { Now = oldNow })

ctx := Context{Database: db}
err = initDatabase(&ctx)
if err != nil {
Expand Down
26 changes: 14 additions & 12 deletions commands/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"strconv"
"strings"
"time"

"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -44,13 +45,14 @@ func newUpdateCommand(ctx *Context) *cobra.Command {
}
id = strings.TrimSuffix(line, "\n")
}
now := Now().Format(time.RFC3339)
if len(machine) > 0 {
query, err := transaction.Prepare("update passwords set machine=? where id=?")
query, err := transaction.Prepare("update passwords set machine=?, modified=? where id=?")
if err != nil {
return fmt.Errorf("db.Prepare() failed: %s", err)
}

result, err := query.Exec(machine, id)
result, err := query.Exec(machine, now, id)
if err != nil {
return fmt.Errorf("db.Exec() failed: %s", err)
}
Expand All @@ -61,12 +63,12 @@ func newUpdateCommand(ctx *Context) *cobra.Command {
}
}
if len(service) > 0 {
query, err := transaction.Prepare("update passwords set service=? where id=?")
query, err := transaction.Prepare("update passwords set service=?, modified=? where id=?")
if err != nil {
return fmt.Errorf("db.Prepare() failed: %s", err)
}

result, err := query.Exec(service, id)
result, err := query.Exec(service, now, id)
if err != nil {
return fmt.Errorf("db.Exec() failed: %s", err)
}
Expand All @@ -77,12 +79,12 @@ func newUpdateCommand(ctx *Context) *cobra.Command {
}
}
if len(user) > 0 {
query, err := transaction.Prepare("update passwords set user=? where id=?")
query, err := transaction.Prepare("update passwords set user=?, modified=? where id=?")
if err != nil {
return fmt.Errorf("db.Prepare() failed: %s", err)
}

result, err := query.Exec(user, id)
result, err := query.Exec(user, now, id)
if err != nil {
return fmt.Errorf("db.Exec() failed: %s", err)
}
Expand All @@ -93,12 +95,12 @@ func newUpdateCommand(ctx *Context) *cobra.Command {
}
}
if len(passwordType) > 0 {
query, err := transaction.Prepare("update passwords set type=? where id=?")
query, err := transaction.Prepare("update passwords set type=?, modified=? where id=?")
if err != nil {
return fmt.Errorf("db.Prepare() failed: %s", err)
}

result, err := query.Exec(passwordType, id)
result, err := query.Exec(passwordType, now, id)
if err != nil {
return fmt.Errorf("db.Exec() failed: %s", err)
}
Expand All @@ -117,12 +119,12 @@ func newUpdateCommand(ctx *Context) *cobra.Command {
}
generatedPassword = true
}
query, err := transaction.Prepare("update passwords set password=? where id=?")
query, err := transaction.Prepare("update passwords set password=?, modified=? where id=?")
if err != nil {
return fmt.Errorf("db.Prepare() failed: %s", err)
}

result, err := query.Exec(password, id)
result, err := query.Exec(password, now, id)
if err != nil {
return fmt.Errorf("db.Exec() failed: %s", err)
}
Expand All @@ -133,7 +135,7 @@ func newUpdateCommand(ctx *Context) *cobra.Command {
}
}
if len(archived) > 0 {
query, err := transaction.Prepare("update passwords set archived=? where id=?")
query, err := transaction.Prepare("update passwords set archived=?, modified=? where id=?")
if err != nil {
return fmt.Errorf("db.Prepare() failed: %s", err)
}
Expand All @@ -142,7 +144,7 @@ func newUpdateCommand(ctx *Context) *cobra.Command {
if err != nil {
return fmt.Errorf("ParseBool() failed: %s", err)
}
result, err := query.Exec(parsed, id)
result, err := query.Exec(parsed, now, id)
if err != nil {
return fmt.Errorf("db.Exec() failed: %s", err)
}
Expand Down
9 changes: 8 additions & 1 deletion commands/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ import (
"fmt"
"os"
"testing"
"time"
)

func NowForTesting() time.Time {
t := "2020-05-10T00:00:00+02:00"
ret, _ := time.Parse(time.RFC3339, t)
return ret
}

func TestUpdate(t *testing.T) {
ctx := CreateContextForTesting(t)
expectedMachine := "mymachine"
Expand Down Expand Up @@ -404,7 +411,7 @@ func TestUpdateArchived(t *testing.T) {
if actualLength != expectedLength {
t.Fatalf("actualLength = %q, want %q", actualLength, expectedLength)
}
actualContains := ContainsString(results, fmt.Sprintf("machine: %s, service: %s, user: %s, password type: plain, password: %s, archived: true", expectedMachine, expectedService, expectedUser, expectedPassword))
actualContains := ContainsString(results, fmt.Sprintf("machine: %s, service: %s, user: %s, password type: plain, password: %s, archived: true, created: 2020-05-10 00:00, modified: 2020-05-10 00:00", expectedMachine, expectedService, expectedUser, expectedPassword))
expectedContains := true
if actualContains != expectedContains {
t.Fatalf("actualContains = %v, want %v", actualContains, expectedContains)
Expand Down
3 changes: 2 additions & 1 deletion guide/src/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ The search term is already specified in this case:
id: 1, machine: example.com, service: http, user: myuser, password type: plain, password: 7U1FvIzubR95Itg
```

Archived passwords are not shown, unless `-v` or `--verbose` is used.
Archived passwords are not shown, unless `-v` or `--verbose` is used. The verbose mode also shows
when the password was created and modified.

## TOTP support

Expand Down

0 comments on commit 3337186

Please sign in to comment.