Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions cmd/andcli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (

tea "charm.land/bubbletea/v2"

"github.com/tjblackheart/andcli/v2/internal/buildinfo"
"github.com/tjblackheart/andcli/v2/internal/config"
"github.com/tjblackheart/andcli/v2/internal/input"
"github.com/tjblackheart/andcli/v2/internal/model"
Expand All @@ -24,7 +23,6 @@ import (

func main() {
log.SetFlags(0)
log.SetPrefix(fmt.Sprintf("%s: ", buildinfo.AppName))

cfg, err := config.Create()
if err != nil {
Expand All @@ -33,7 +31,7 @@ func main() {

vault, err := open(cfg)
if err != nil {
log.Fatalln(err)
log.Fatalf("Error reading file: %s\n", err)
}

entries := vault.Entries()
Expand Down
16 changes: 16 additions & 0 deletions internal/vaults/aegis/aegis.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ func Open(filename string, pass []byte) (vaults.Vault, error) {
return nil, fmt.Errorf("%s: %w", vaultType, err)
}

if v.IsPlain(b) {
return nil, vaults.ErrIsPlain
}

if err := json.Unmarshal(b, &v); err != nil {
return nil, fmt.Errorf("%s: %w", vaultType, err)
}
Expand Down Expand Up @@ -109,6 +113,18 @@ func (v aegis) Entries() []vaults.Entry {
return entries
}

func (v aegis) IsPlain(b []byte) bool {
var db struct {
DB json.RawMessage `json:"db"`
}

json.Unmarshal(b, &db) // intentional skip err check
if len(db.DB) > 0 && db.DB[0] != '"' {
return true
}
return false
}

func (v aegis) masterKeyFromPass(password []byte) ([]byte, error) {
var salt, keyNonce, keyTag, key, derivedKey []byte
var err error
Expand Down
22 changes: 14 additions & 8 deletions internal/vaults/aegis/aegis_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package aegis

import (
"errors"
"reflect"
"testing"

Expand All @@ -13,21 +14,26 @@ func TestOpen(t *testing.T) {
filename string
password string
fails bool
wantErr error
}{
{"decrypts", "testdata/aegis-export-test.json", "andcli-test", false},
{"fails: wrong password", "testdata/aegis-export-test.json", "invalid", true},
{"fails: invalid file", "testdata/aegis-invalid-file.json", "invalid", true},
{"decrypts", "testdata/aegis-export-test.json", "andcli-test", false, nil},
{"fails: wrong password", "testdata/aegis-export-test.json", "invalid", true, nil},
{"fails: invalid file", "testdata/aegis-invalid-file.json", "invalid", true, nil},
{"fails: plaintext vault", "testdata/aegis-export-test-plain.json", "", true, vaults.ErrIsPlain},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v, err := Open(tt.filename, []byte(tt.password))
if tt.fails {
if err == nil {
t.Fatal("Open() expected error, got none")
}
return
if tt.fails {
if err == nil {
t.Fatal("Open() expected error, got none")
}
if tt.wantErr != nil && !errors.Is(err, tt.wantErr) {
t.Fatalf("Open() error = %v, want %v", err, tt.wantErr)
}
return
}

entries := v.Entries()
if len(entries) != 1 {
Expand Down
13 changes: 13 additions & 0 deletions internal/vaults/aegis/testdata/aegis-export-test-plain.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"header": {
"slots": null,
"params": null
},
"db": {
"version": 3,
"entries": [],
"groups": [],
"icons_optimized": true
}
}
15 changes: 12 additions & 3 deletions internal/vaults/andotp/andotp.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,22 @@ func Open(filename string, pass []byte) (vaults.Vault, error) {
return nil, fmt.Errorf("%s: %w", vaultType, err)
}

v := &andotp{entries: make([]entry, 0)}

if v.IsPlain(b) {
return nil, vaults.ErrIsPlain
}

b, err = gao.Decrypt(b, string(pass))
if err != nil {
return nil, fmt.Errorf("%s: %w", vaultType, err)
}

entries := make([]entry, 0)
if err := json.Unmarshal(b, &entries); err != nil {
if err := json.Unmarshal(b, &v.entries); err != nil {
return nil, fmt.Errorf("%s: %w", vaultType, err)
}

return &andotp{entries}, nil
return v, nil
}

func (v andotp) Entries() []vaults.Entry {
Expand All @@ -73,3 +78,7 @@ func (v andotp) Entries() []vaults.Entry {

return entries
}

func (v andotp) IsPlain(b []byte) bool {
return b[0] == '['
}
20 changes: 13 additions & 7 deletions internal/vaults/andotp/andotp_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package andotp

import (
"errors"
"reflect"
"testing"

Expand All @@ -13,20 +14,25 @@ func TestOpen(t *testing.T) {
filename string
password string
fails bool
wantErr error
}{
{"decrypts", "testdata/andotp_test.json.aes", "andcli-test", false},
{"fails: wrong password", "testdata/andotp_test.json.aes", "invalid", true},
{"decrypts", "testdata/andotp_test.json.aes", "andcli-test", false, nil},
{"fails: wrong password", "testdata/andotp_test.json.aes", "invalid", true, nil},
{"fails: plaintext vault", "testdata/andotp_test_plain.json", "", true, vaults.ErrIsPlain},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v, err := Open(tt.filename, []byte(tt.password))
if tt.fails {
if err == nil {
t.Fatal("Open() expected error, got none")
}
return
if tt.fails {
if err == nil {
t.Fatal("Open() expected error, got none")
}
if tt.wantErr != nil && !errors.Is(err, tt.wantErr) {
t.Fatalf("Open() error = %v, want %v", err, tt.wantErr)
}
return
}

entries := v.Entries()
if len(entries) != 1 {
Expand Down
15 changes: 15 additions & 0 deletions internal/vaults/andotp/testdata/andotp_test_plain.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[
{
"secret": "ZLM4YMN5NTRNG7KHM45PYKSVUROQZQ5CFA5OH6AQ5TMWOBENLSTA====",
"issuer": "otp.nwo.dev",
"label": "andcli-test",
"digits": 6,
"type": "TOTP",
"algorithm": "SHA256",
"thumbnail": "Default",
"last_used": 1667481235360,
"used_frequency": 0,
"period": 30,
"tags": []
}
]
4 changes: 4 additions & 0 deletions internal/vaults/keepass/keepass.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ func (v keepass) Entries() []vaults.Entry {
return entries
}

func (v keepass) IsPlain(b []byte) bool {
return false // not sure if you even can export an unecrypted kdbx.
}

func parseGroups(groups []gokeepasslib.Group) []gokeepasslib.Entry {
entries := make([]gokeepasslib.Entry, 0)
for _, group := range groups {
Expand Down
19 changes: 14 additions & 5 deletions internal/vaults/protonpass/protonpass.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import (

const vaultType = vaults.PROTON

var _ vaults.Vault = &envelope{}
var (
_ vaults.Vault = &envelope{}
zipHeader = []byte{0x50, 0x4b, 0x03, 0x04}
)

type (
envelope struct{ Vaults map[string]proton }
Expand All @@ -46,6 +49,11 @@ func Open(filename string, pass []byte) (vaults.Vault, error) {
return nil, fmt.Errorf("%s: %s", vaultType, err)
}

var e envelope
if e.IsPlain(b) {
return nil, vaults.ErrIsPlain
}

hnd, err := crypto.PGP().Decryption().Password(pass).New()
if err != nil {
return nil, fmt.Errorf("%s: %s", vaultType, err)
Expand All @@ -56,7 +64,6 @@ func Open(filename string, pass []byte) (vaults.Vault, error) {
return nil, fmt.Errorf("%s: %s", vaultType, err)
}

var e envelope
if err := json.Unmarshal(result.Bytes(), &e); err != nil {
return nil, fmt.Errorf("%s: %s", vaultType, err)
}
Expand Down Expand Up @@ -103,10 +110,12 @@ func (e envelope) Entries() []vaults.Entry {
return entries
}

func (e envelope) IsPlain(b []byte) bool {
return b[0] == '{' || string(b[:4]) == "type"
}

// opens, reads and returns file content, handles zip if necessary.
func read(filename string) ([]byte, error) {
sig := []byte{0x50, 0x4b, 0x03, 0x04}

f, err := os.Open(filename)
if err != nil {
return nil, err
Expand All @@ -119,7 +128,7 @@ func read(filename string) ([]byte, error) {
}

// not a zip file
if !bytes.Equal(head, sig) {
if !bytes.Equal(head, zipHeader) {
return os.ReadFile(filename)
}

Expand Down
26 changes: 17 additions & 9 deletions internal/vaults/protonpass/protonpass_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package protonpass

import (
"errors"
"fmt"
"testing"

"github.com/tjblackheart/andcli/v2/internal/vaults"
)

func TestMain(m *testing.M) {
Expand All @@ -15,22 +18,27 @@ func TestOpen(t *testing.T) {
filename string
password string
fails bool
wantErr error
}{
{"decrypts text", "testdata/protonpass-test.pgp", "andcli-test", false},
{"decrypts zip", "testdata/protonpass-test.pgp.zip", "andcli-test", false},
{"decrypts hidden zip", "testdata/protonpass-test.pgp.data", "andcli-test", false},
{"fails: wrong password", "testdata/protonpass-test.pgp", "", true},
{"decrypts text", "testdata/protonpass-test.pgp", "andcli-test", false, nil},
{"decrypts zip", "testdata/protonpass-test.pgp.zip", "andcli-test", false, nil},
{"decrypts hidden zip", "testdata/protonpass-test.pgp.data", "andcli-test", false, nil},
{"fails: wrong password", "testdata/protonpass-test.pgp", "", true, nil},
{"fails: plaintext vault", "testdata/protonpass-test-plain.zip", "", true, vaults.ErrIsPlain},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v, err := Open(tt.filename, []byte(tt.password))
if tt.fails {
if err == nil {
t.Fatal("Open() expected error, got nil")
}
return
if tt.fails {
if err == nil {
t.Fatal("Open() expected error, got none")
}
if tt.wantErr != nil && !errors.Is(err, tt.wantErr) {
t.Fatalf("Open() error = %v, want %v", err, tt.wantErr)
}
return
}

entries := v.Entries()
if len(entries) != 3 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
type,name,url,email,username,password,note,totp,createTime,modifyTime,vault
Binary file not shown.
7 changes: 7 additions & 0 deletions internal/vaults/stratum/stratum.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ func Open(filename string, pass []byte) (vaults.Vault, error) {
}

v := &stratum{Authenticators: make([]entry, 0)}
if v.IsPlain(b) {
return nil, vaults.ErrIsPlain
}

switch string(b[:len(HEADER)]) {
case HEADER:
Expand Down Expand Up @@ -125,6 +128,10 @@ func (v stratum) Entries() []vaults.Entry {
return list
}

func (v stratum) IsPlain(b []byte) bool {
return len(b) >= 7 && string(b[:7]) == "otpauth"
}

func (v stratum) decrypt(b, pass []byte) ([]byte, error) {
salt := b[len(HEADER) : len(HEADER)+SALT_LENGTH]
nonce := b[len(HEADER)+SALT_LENGTH : len(HEADER)+SALT_LENGTH+IV_LENGTH]
Expand Down
22 changes: 14 additions & 8 deletions internal/vaults/stratum/stratum_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package stratum

import (
"errors"
"fmt"
"reflect"
"testing"
Expand All @@ -18,21 +19,26 @@ func TestOpen(t *testing.T) {
filename string
password string
fails bool
wantErr error
}{
{"decrypts", "testdata/backup-andcli-test.stratum", "andcli-test", false},
{"fails: wrong password", "testdata/backup-andcli-test.stratum", "", true},
{"fails: legacy", "testdata/backup-legacy-andcli-test.stratum", "", true},
{"decrypts", "testdata/backup-andcli-test.stratum", "andcli-test", false, nil},
{"fails: wrong password", "testdata/backup-andcli-test.stratum", "", true, nil},
{"fails: legacy", "testdata/backup-legacy-andcli-test.stratum", "", true, nil},
{"fails: plaintext vault", "testdata/backup-andcli-test-plain.stratum.txt", "", true, vaults.ErrIsPlain},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v, err := Open(tt.filename, []byte(tt.password))
if tt.fails {
if err == nil {
t.Fatal("Open() expected error, got none")
}
return
if tt.fails {
if err == nil {
t.Fatal("Open() expected error, got none")
}
if tt.wantErr != nil && !errors.Is(err, tt.wantErr) {
t.Fatalf("Open() error = %v, want %v", err, tt.wantErr)
}
return
}

entries := v.Entries()
if len(entries) != 3 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
otpauth://totp/otp.provider.dev%3Atester?secret=abc123&issuer=otp.provider.dev
Loading
Loading