diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b261c7..3187120 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,20 @@
# Changelog - Paw
+## 0.17.0 - 29 March 2022
+
+- all: add Ed25519 and RSA SSH keys support
+- deps add:
+ - github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a
+- deps upgrade:
+ - fyne.io/fyne v2.1.4
+ - golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
+ - golang.org/x/image v0.0.0-20220321031419-a8550c1d254a
+ - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
+ - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
+
## 0.16.1 - 08 March 2022
-gui: fix item creation should show default content on cancel
+- gui: fix item creation should show default content on cancel
## 0.16.0 - 28 February 2022
diff --git a/README.md b/README.md
index 309914a..b720296 100644
--- a/README.md
+++ b/README.md
@@ -112,6 +112,7 @@ Currently the following items are available:
- login
- note
- password
+- ssh_key
## Threat model
diff --git a/go.mod b/go.mod
index 802c349..623651f 100644
--- a/go.mod
+++ b/go.mod
@@ -4,11 +4,12 @@ go 1.16
require (
filippo.io/age v1.0.0
- fyne.io/fyne/v2 v2.1.3
- github.com/stretchr/testify v1.7.0
+ fyne.io/fyne/v2 v2.1.4
+ github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a
+ github.com/stretchr/testify v1.7.1
golang.design/x/clipboard v0.6.0
- golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
- golang.org/x/image v0.0.0-20211028202545-6944b10bf410
+ golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
+ golang.org/x/image v0.0.0-20220321031419-a8550c1d254a
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
- golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
+ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
)
diff --git a/go.sum b/go.sum
index dbdde74..149a38a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,8 @@
filippo.io/age v1.0.0 h1:V6q14n0mqYU3qKFkZ6oOaF9oXneOviS3ubXsSVBRSzc=
filippo.io/age v1.0.0/go.mod h1:PaX+Si/Sd5G8LgfCwldsSba3H1DDQZhIhFGkhbHaBq8=
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
-fyne.io/fyne/v2 v2.1.3 h1:I5qSeENAcq67hmO5Z2hI7sEJm9bdLMDJx59Fv8qJkX0=
-fyne.io/fyne/v2 v2.1.3/go.mod h1:p+E/Dh+wPW8JwR2DVcsZ9iXgR9ZKde80+Y+40Is54AQ=
+fyne.io/fyne/v2 v2.1.4 h1:bt1+28++kAzRzPB0GM2EuSV4cnl8rXNX4cjfd8G06Rc=
+fyne.io/fyne/v2 v2.1.4/go.mod h1:p+E/Dh+wPW8JwR2DVcsZ9iXgR9ZKde80+Y+40Is54AQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -31,6 +31,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc=
+github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
+github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
@@ -49,8 +51,8 @@ github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.3.8 h1:Nw158Q8QN+CPgTmVRByhVwapp8Mm1e2blinhmx4wx5E=
@@ -60,16 +62,17 @@ golang.design/x/clipboard v0.6.0/go.mod h1:ep0pB+/4DGJK3ayLxweWJFHhHGGv3npJJHMXA
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s=
+golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
-golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
-golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20220321031419-a8550c1d254a h1:LnH9RNcpPv5Kzi15lXg42lYMPUf0x8CuPv1YnvBWZAg=
+golang.org/x/image v0.0.0-20220321031419-a8550c1d254a/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 h1:3In5TnfvnuXTF/uflgpYxSCEGP2NdYT37KsPh3VjZYU=
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554/go.mod h1:jFTmtFYCV0MFtXBU+J5V/+5AUeVS0ON/0WkE/KSrl6E=
@@ -79,8 +82,9 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -90,14 +94,16 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
diff --git a/internal/cli/cmd_add.go b/internal/cli/cmd_add.go
index cb2d4b2..f199857 100644
--- a/internal/cli/cmd_add.go
+++ b/internal/cli/cmd_add.go
@@ -5,6 +5,7 @@ import (
"os"
"lucor.dev/paw/internal/paw"
+ "lucor.dev/paw/internal/sshkey"
)
// Add adds an item to the vault
@@ -83,6 +84,8 @@ func (cmd *AddCmd) Run(s paw.Storage) error {
cmd.addNoteItem(item)
case paw.PasswordItemType:
cmd.addPasswordItem(vault.Key(), item)
+ case paw.SSHKeyItemType:
+ cmd.addSSHKeyItem(item)
default:
return fmt.Errorf("unsupported item type: %q", cmd.itemType)
}
@@ -185,3 +188,25 @@ func (cmd *AddCmd) addPasswordItem(key *paw.Key, item paw.Item) error {
item = v
return nil
}
+
+func (cmd *AddCmd) addSSHKeyItem(item paw.Item) error {
+ v := item.(*paw.SSHKey)
+
+ k, err := sshkey.GenerateKey()
+ if err != nil {
+ return err
+ }
+
+ v.PrivateKey = string(k.PrivateKey())
+ v.PublicKey = string(k.PublicKey())
+ v.Fingerprint = k.Fingerprint()
+
+ note, err := ask("Note")
+ if err != nil {
+ return err
+ }
+
+ v.Note.Value = note
+ item = v
+ return nil
+}
diff --git a/internal/cli/cmd_edit.go b/internal/cli/cmd_edit.go
index 1b4ad99..549b6cd 100644
--- a/internal/cli/cmd_edit.go
+++ b/internal/cli/cmd_edit.go
@@ -85,6 +85,8 @@ func (cmd *EditCmd) Run(s paw.Storage) error {
cmd.editNoteItem(item)
case paw.PasswordItemType:
cmd.editPasswordItem(vault.Key(), item)
+ case paw.SSHKeyItemType:
+ cmd.editSSHKeyItem(item)
default:
return fmt.Errorf("unsupported item type: %q", cmd.itemType)
}
@@ -189,3 +191,16 @@ func (cmd *EditCmd) editPasswordItem(key *paw.Key, item paw.Item) error {
item = v
return nil
}
+
+func (cmd *EditCmd) editSSHKeyItem(item paw.Item) error {
+ v := item.(*paw.SSHKey)
+
+ note, err := askWithDefault("Note", v.Note.Value)
+ if err != nil {
+ return err
+ }
+
+ v.Note.Value = note
+ item = v
+ return nil
+}
diff --git a/internal/cli/cmd_list.go b/internal/cli/cmd_list.go
index d266121..9f6a9e6 100644
--- a/internal/cli/cmd_list.go
+++ b/internal/cli/cmd_list.go
@@ -113,6 +113,7 @@ func (cmd *ListCmd) items(s paw.Storage) ([]tree.Node, error) {
loginNode := tree.Node{Value: paw.LoginItemType.String()}
noteNode := tree.Node{Value: paw.NoteItemType.String()}
passwordNode := tree.Node{Value: paw.PasswordItemType.String()}
+ sshkeyNode := tree.Node{Value: paw.SSHKeyItemType.String()}
for _, v := range meta {
switch v.Type {
case paw.LoginItemType:
@@ -121,6 +122,8 @@ func (cmd *ListCmd) items(s paw.Storage) ([]tree.Node, error) {
noteNode.Child = append(noteNode.Child, tree.Node{Value: v.Name})
case paw.PasswordItemType:
passwordNode.Child = append(passwordNode.Child, tree.Node{Value: v.Name})
+ case paw.SSHKeyItemType:
+ sshkeyNode.Child = append(sshkeyNode.Child, tree.Node{Value: v.Name})
}
}
@@ -128,6 +131,7 @@ func (cmd *ListCmd) items(s paw.Storage) ([]tree.Node, error) {
loginNode,
noteNode,
passwordNode,
+ sshkeyNode,
}, nil
}
diff --git a/internal/cli/cmd_show.go b/internal/cli/cmd_show.go
index 2aa6419..7518c03 100644
--- a/internal/cli/cmd_show.go
+++ b/internal/cli/cmd_show.go
@@ -94,6 +94,7 @@ func (cmd *ShowCmd) Run(s paw.Storage) error {
}
var pclip []byte
+ var pclipMsg string
switch cmd.itemType {
case paw.LoginItemType:
v := item.(*paw.Login)
@@ -103,6 +104,7 @@ func (cmd *ShowCmd) Run(s paw.Storage) error {
fmt.Printf("Password: %s\n", v.Password.Value)
} else {
pclip = []byte(v.Password.Value)
+ pclipMsg = "[✓] password copied to clipboard"
}
if v.Note != nil {
fmt.Printf("Note: %s\n", v.Note.Value)
@@ -113,10 +115,24 @@ func (cmd *ShowCmd) Run(s paw.Storage) error {
fmt.Printf("Password: %s\n", v.Value)
} else {
pclip = []byte(v.Value)
+ pclipMsg = "[✓] password copied to clipboard"
}
if v.Note != nil {
fmt.Printf("Note: %s\n", v.Note.Value)
}
+ case paw.SSHKeyItemType:
+ v := item.(*paw.SSHKey)
+ if !cmd.clipboard {
+ fmt.Printf("Private key: %s\n", v.PrivateKey)
+ } else {
+ pclip = []byte(v.PrivateKey)
+ pclipMsg = "[✓] private key copied to clipboard"
+ }
+ fmt.Printf("Public key: %s\n", v.PublicKey)
+ fmt.Printf("Fingerprint: %s\n", v.Fingerprint)
+ if v.Note != nil {
+ fmt.Printf("Note: %s\n", v.Note.Value)
+ }
case paw.NoteItemType:
v := item.(*paw.Note)
fmt.Printf("Note: %s\n", v.Value)
@@ -132,7 +148,7 @@ func (cmd *ShowCmd) Run(s paw.Storage) error {
if err != nil {
return nil
}
- fmt.Println("[✓] password copied to clipboard")
+ fmt.Println(pclipMsg)
}
return nil
}
diff --git a/internal/icon/download_outlined.go b/internal/icon/download_outlined.go
new file mode 100644
index 0000000..a0f34d3
--- /dev/null
+++ b/internal/icon/download_outlined.go
@@ -0,0 +1,18 @@
+// auto-generated
+// Code generated by 'fynematic'. DO NOT EDIT.
+
+package icon
+
+import "fyne.io/fyne/v2"
+
+var DownloadOutlinedIconThemed = NewThemedResource(DownloadOutlinedIconDarkRes, DownloadOutlinedIconLightRes)
+
+var DownloadOutlinedIconDarkRes = &fyne.StaticResource{
+ StaticName: "download_outlined_dark.svg",
+ StaticContent: []byte(""),
+}
+
+var DownloadOutlinedIconLightRes = &fyne.StaticResource{
+ StaticName: "download_outlined_light.svg",
+ StaticContent: []byte(""),
+}
diff --git a/internal/icon/upload_outlined.go b/internal/icon/upload_outlined.go
new file mode 100644
index 0000000..b8c77f2
--- /dev/null
+++ b/internal/icon/upload_outlined.go
@@ -0,0 +1,18 @@
+// auto-generated
+// Code generated by 'fynematic'. DO NOT EDIT.
+
+package icon
+
+import "fyne.io/fyne/v2"
+
+var UploadOutlinedIconThemed = NewThemedResource(UploadOutlinedIconDarkRes, UploadOutlinedIconLightRes)
+
+var UploadOutlinedIconDarkRes = &fyne.StaticResource{
+ StaticName: "upload_outlined_dark.svg",
+ StaticContent: []byte(""),
+}
+
+var UploadOutlinedIconLightRes = &fyne.StaticResource{
+ StaticName: "upload_outlined_light.svg",
+ StaticContent: []byte(""),
+}
diff --git a/internal/paw/item.go b/internal/paw/item.go
index ae0ce41..f8f3f45 100644
--- a/internal/paw/item.go
+++ b/internal/paw/item.go
@@ -16,6 +16,8 @@ const (
PasswordItemType
// LoginItemType is the Website Item type
LoginItemType
+ // SSHKeyItemType is the SSH Key Item type
+ SSHKeyItemType
)
func (it ItemType) String() string {
@@ -28,6 +30,8 @@ func (it ItemType) String() string {
return "password"
case LoginItemType:
return "login"
+ case SSHKeyItemType:
+ return "ssh_key"
}
return "invalid"
}
@@ -42,6 +46,8 @@ func ItemTypeFromString(v string) (ItemType, error) {
itemType = NoteItemType
case PasswordItemType.String():
itemType = PasswordItemType
+ case SSHKeyItemType.String():
+ itemType = SSHKeyItemType
default:
err = fmt.Errorf("invalid item type %q", v)
}
@@ -67,6 +73,8 @@ func NewItem(name string, itemType ItemType) (Item, error) {
item = NewNote()
case PasswordItemType:
item = NewPassword()
+ case SSHKeyItemType:
+ item = NewSSHKey()
default:
return nil, fmt.Errorf("invalid item type %q", itemType)
}
diff --git a/internal/paw/item_ssh_key.go b/internal/paw/item_ssh_key.go
new file mode 100644
index 0000000..bd7ae5b
--- /dev/null
+++ b/internal/paw/item_ssh_key.go
@@ -0,0 +1,29 @@
+package paw
+
+import (
+ "time"
+)
+
+// Declare conformity to Item interface
+var _ Item = (*SSHKey)(nil)
+
+type SSHKey struct {
+ *Metadata `json:"metadata,omitempty"`
+ *Note `json:"note,omitempty"`
+
+ Fingerprint string `json:"fingerprint,omitempty"`
+ PrivateKey string `json:"private_key,omitempty"`
+ PublicKey string `json:"public_key,omitempty"`
+}
+
+func NewSSHKey() *SSHKey {
+ now := time.Now()
+ return &SSHKey{
+ Metadata: &Metadata{
+ Type: SSHKeyItemType,
+ Created: now,
+ Modified: now,
+ },
+ Note: &Note{},
+ }
+}
diff --git a/internal/paw/paw.go b/internal/paw/paw.go
index 7a5575e..06f53bc 100644
--- a/internal/paw/paw.go
+++ b/internal/paw/paw.go
@@ -186,7 +186,7 @@ func (k *Key) Secret(seeder Seeder) (string, error) {
// decode the age identity to be used as secret for HKDF function
_, data, err := bech32.Decode(k.ageIdentity.String())
if err != nil {
- return "", fmt.Errorf("could not decode the age identity %w", err)
+ panic(fmt.Sprintf("could not decode the age identity %s", err))
}
// reader to derive a key
diff --git a/internal/paw/storage_os.go b/internal/paw/storage_os.go
index 8a54b10..9454319 100644
--- a/internal/paw/storage_os.go
+++ b/internal/paw/storage_os.go
@@ -172,6 +172,8 @@ func (s *OSStorage) LoadItem(vault *Vault, itemMetadata *Metadata) (Item, error)
item = &Password{}
case LoginItemType:
item = &Login{}
+ case SSHKeyItemType:
+ item = &SSHKey{}
}
itemFile := itemPath(s, vault.Name, itemMetadata.ID())
diff --git a/internal/sshkey/sshkey.go b/internal/sshkey/sshkey.go
new file mode 100644
index 0000000..58b32bb
--- /dev/null
+++ b/internal/sshkey/sshkey.go
@@ -0,0 +1,79 @@
+package sshkey
+
+import (
+ "crypto"
+ "crypto/ed25519"
+ cryptorand "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+
+ "github.com/mikesmitty/edkey"
+ "golang.org/x/crypto/ssh"
+)
+
+// GenerateKey generates an ed25519 sshkey
+func GenerateKey() (sshkey, error) {
+ pubKey, privKey, err := ed25519.GenerateKey(cryptorand.Reader)
+ if err != nil {
+ return sshkey{}, fmt.Errorf("could not generate ed25519 key: %w", err)
+ }
+ return sshkey{privateKey: &privKey, publicKey: pubKey}, nil
+}
+
+// ParseKey parses a raw RSA or Ed22519 ssh key
+func ParseKey(b []byte) (sshkey, error) {
+ k, err := ssh.ParseRawPrivateKey(b)
+ if err != nil {
+ return sshkey{}, err
+ }
+ switch v := k.(type) {
+ case *ed25519.PrivateKey:
+ return sshkey{privateKey: v, publicKey: v.Public()}, err
+ case *rsa.PrivateKey:
+ return sshkey{privateKey: v, publicKey: v.Public()}, err
+ default:
+ return sshkey{}, fmt.Errorf("unsupported type %T", v)
+ }
+}
+
+type sshkey struct {
+ privateKey crypto.PrivateKey
+ publicKey crypto.PublicKey
+}
+
+func (sk sshkey) PrivateKey() []byte {
+ var pemBlock *pem.Block
+ switch v := sk.privateKey.(type) {
+ case *ed25519.PrivateKey:
+ // TODO move to x/crypto/ssh once https://go-review.googlesource.com/c/crypto/+/218620/ is merged
+ // see golang/go#37132
+ pemBlock = &pem.Block{
+ Type: "OPENSSH PRIVATE KEY",
+ Bytes: edkey.MarshalED25519PrivateKey(*v),
+ }
+ case *rsa.PrivateKey:
+ pemBlock = &pem.Block{
+ Type: "RSA PRIVATE KEY",
+ Bytes: x509.MarshalPKCS1PrivateKey(v),
+ }
+ }
+ return pem.EncodeToMemory(pemBlock)
+}
+
+func (sk sshkey) sshPublicKey() ssh.PublicKey {
+ sshPublicKey, err := ssh.NewPublicKey(sk.publicKey)
+ if err != nil {
+ panic("could not generate ssh public key from the crypto public key")
+ }
+ return sshPublicKey
+}
+
+func (sk sshkey) PublicKey() []byte {
+ return ssh.MarshalAuthorizedKey(sk.sshPublicKey())
+}
+
+func (sk sshkey) Fingerprint() string {
+ return ssh.FingerprintSHA256(sk.sshPublicKey())
+}
diff --git a/internal/ui/dialog.go b/internal/ui/dialog.go
deleted file mode 100644
index d36fd87..0000000
--- a/internal/ui/dialog.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package ui
-
-import (
- "fmt"
-
- "fyne.io/fyne/v2"
- "fyne.io/fyne/v2/dialog"
- "fyne.io/fyne/v2/widget"
-)
-
-func ShowErrorDialog(title string, err error, w fyne.Window) {
- content := &widget.Label{
- Text: fmt.Sprintf("Error: %s", err),
- Wrapping: fyne.TextWrapBreak,
- }
-
- d := dialog.NewCustom(title, "Ok", content, w)
- d.Show()
-}
diff --git a/internal/ui/item_fyne.go b/internal/ui/item_fyne.go
index 9f3fd5c..de13e58 100644
--- a/internal/ui/item_fyne.go
+++ b/internal/ui/item_fyne.go
@@ -10,9 +10,11 @@ import (
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
+ "fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
+ "lucor.dev/paw/internal/icon"
"lucor.dev/paw/internal/paw"
)
@@ -42,7 +44,8 @@ func NewFyneItem(item paw.Item) FyneItem {
fyneItem = &Login{Login: item.(*paw.Login)}
case paw.PasswordItemType:
fyneItem = &Password{Password: item.(*paw.Password)}
-
+ case paw.SSHKeyItemType:
+ fyneItem = &SSHKey{SSHKey: item.(*paw.SSHKey)}
}
return fyneItem
}
@@ -63,8 +66,55 @@ func labelWithStyle(label string) *widget.Label {
return widget.NewLabelWithStyle(label, fyne.TextAlignTrailing, fyne.TextStyle{Bold: true})
}
+type rowActions struct {
+ copy bool
+ ellipsis int
+ export string
+}
+
+func rowWithAction(label string, text string, actions rowActions, w fyne.Window) []fyne.CanvasObject {
+ labelText := text
+ if actions.ellipsis > 0 {
+ labelText = text[0:actions.ellipsis] + "..."
+ }
+ t := widget.NewLabel(labelText)
+ t.Wrapping = fyne.TextWrapBreak
+
+ c := container.NewVBox()
+ if actions.copy {
+ b := widget.NewButtonWithIcon("Copy", theme.ContentCopyIcon(), func() {
+ w.Clipboard().SetContent(text)
+ fyne.CurrentApp().SendNotification(&fyne.Notification{
+ Title: "paw",
+ Content: fmt.Sprintf("%s copied", label),
+ })
+ })
+ c.Add(b)
+ }
+
+ if actions.export != "" {
+ b := widget.NewButtonWithIcon("Export", icon.DownloadOutlinedIconThemed, func() {
+ d := dialog.NewFileSave(func(uc fyne.URIWriteCloser, err error) {
+ if uc == nil {
+ // file open dialog has been cancelled
+ return
+ }
+ defer uc.Close()
+ uc.Write([]byte(text))
+ }, w)
+ d.SetFileName(actions.export)
+ d.Show()
+ })
+ c.Add(b)
+ }
+
+ l := labelWithStyle(label)
+ return []fyne.CanvasObject{l, container.NewBorder(nil, nil, nil, c, t)}
+}
+
func copiableRow(label string, text string, w fyne.Window) []fyne.CanvasObject {
t := widget.NewLabel(text)
+ t.Wrapping = fyne.TextWrapBreak
b := widget.NewButtonWithIcon("Copy", theme.ContentCopyIcon(), func() {
w.Clipboard().SetContent(text)
fyne.CurrentApp().SendNotification(&fyne.Notification{
@@ -74,7 +124,7 @@ func copiableRow(label string, text string, w fyne.Window) []fyne.CanvasObject {
})
l := labelWithStyle(label)
- return []fyne.CanvasObject{l, container.NewBorder(nil, nil, nil, b, t)}
+ return []fyne.CanvasObject{l, container.NewBorder(nil, nil, nil, container.NewVBox(b), t)}
}
func copiableLinkRow(label string, text string, w fyne.Window) []fyne.CanvasObject {
diff --git a/internal/ui/item_metadata.go b/internal/ui/item_metadata.go
index 2f9260e..063a96a 100644
--- a/internal/ui/item_metadata.go
+++ b/internal/ui/item_metadata.go
@@ -35,6 +35,8 @@ func (m *Metadata) Icon() fyne.Resource {
return icon.PasswordOutlinedIconThemed
case paw.LoginItemType:
return icon.PublicOutlinedIconThemed
+ case paw.SSHKeyItemType:
+ return icon.KeyOutlinedIconThemed
}
return icon.PawIcon
}
diff --git a/internal/ui/item_sshkey.go b/internal/ui/item_sshkey.go
new file mode 100644
index 0000000..d70636b
--- /dev/null
+++ b/internal/ui/item_sshkey.go
@@ -0,0 +1,183 @@
+package ui
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/data/binding"
+ "fyne.io/fyne/v2/dialog"
+ "fyne.io/fyne/v2/layout"
+ "fyne.io/fyne/v2/theme"
+ "fyne.io/fyne/v2/widget"
+
+ "lucor.dev/paw/internal/icon"
+ "lucor.dev/paw/internal/paw"
+ "lucor.dev/paw/internal/sshkey"
+)
+
+// Declare conformity to Item interface
+var _ paw.Item = (*Password)(nil)
+
+// Declare conformity to FyneItem interface
+var _ FyneItem = (*Password)(nil)
+
+type SSHKey struct {
+ *paw.SSHKey
+}
+
+func (sh *SSHKey) Item() paw.Item {
+ return sh.SSHKey
+}
+
+func (sh *SSHKey) Icon() fyne.Resource {
+ if sh.Favicon != nil {
+ return sh.Favicon
+ }
+ return icon.KeyOutlinedIconThemed
+}
+
+func (sh *SSHKey) Edit(ctx context.Context, key *paw.Key, w fyne.Window) (fyne.CanvasObject, paw.Item) {
+ sshKeyItem := &paw.SSHKey{}
+ *sshKeyItem = *sh.SSHKey
+ sshKeyItem.Metadata = &paw.Metadata{}
+ *sshKeyItem.Metadata = *sh.Metadata
+ sshKeyItem.Note = &paw.Note{}
+ *sshKeyItem.Note = *sh.Note
+
+ titleEntryBind := binding.BindString(&sshKeyItem.Name)
+ titleEntry := widget.NewEntryWithData(titleEntryBind)
+ titleEntry.Validator = nil
+ titleEntry.PlaceHolder = "Untitled SSH Key"
+
+ publicKeyEntryBind := binding.BindString(&sshKeyItem.PublicKey)
+ publicKeyEntry := widget.NewEntryWithData(publicKeyEntryBind)
+ publicKeyEntry.Validator = nil
+ publicKeyEntry.MultiLine = true
+ publicKeyEntry.Wrapping = fyne.TextWrapBreak
+ publicKeyEntry.Disable()
+ publicKeyCopyButton := widget.NewButtonWithIcon("Copy", theme.ContentCopyIcon(), func() {
+ w.Clipboard().SetContent(publicKeyEntry.Text)
+ fyne.CurrentApp().SendNotification(&fyne.Notification{
+ Title: "paw",
+ Content: "Public Key copied to clipboard",
+ })
+ })
+ publicKeyExportButton := widget.NewButtonWithIcon("Export", icon.DownloadOutlinedIconThemed, func() {
+ d := dialog.NewFileSave(func(uc fyne.URIWriteCloser, err error) {
+ if uc == nil {
+ // file open dialog has been cancelled
+ return
+ }
+ defer uc.Close()
+ v, err := publicKeyEntryBind.Get()
+ if err != nil {
+ dialog.NewError(err, w).Show()
+ return
+ }
+ uc.Write([]byte(v))
+ }, w)
+ filename, _ := titleEntryBind.Get()
+ d.SetFileName(fmt.Sprintf("%s.pub", filename))
+ d.Show()
+ })
+
+ fingerprintEntryBind := binding.BindString(&sshKeyItem.Fingerprint)
+ fingerprintEntry := widget.NewLabelWithData(fingerprintEntryBind)
+
+ privateKeyEntryBind := binding.BindString(&sshKeyItem.PrivateKey)
+ privateKeyEntry := widget.NewEntryWithData(privateKeyEntryBind)
+ privateKeyEntry.Validator = nil
+ privateKeyEntry.MultiLine = true
+ privateKeyEntry.Disable()
+ privateKeyEntry.SetPlaceHolder("Private Key")
+ privateKeyCopyButton := widget.NewButtonWithIcon("Copy", theme.ContentCopyIcon(), func() {
+ w.Clipboard().SetContent(privateKeyEntry.Text)
+ fyne.CurrentApp().SendNotification(&fyne.Notification{
+ Title: "paw",
+ Content: "Private Key copied to clipboard",
+ })
+ })
+ privateKeyMakeButton := widget.NewButtonWithIcon("Generate", icon.KeyOutlinedIconThemed, func() {
+ sk, err := sshkey.GenerateKey()
+ if err != nil {
+ dialog.NewError(err, w).Show()
+ return
+ }
+ privateKeyEntryBind.Set(string(sk.PrivateKey()))
+ publicKeyEntryBind.Set(string(sk.PublicKey()))
+ fingerprintEntryBind.Set(string(sk.Fingerprint()))
+ })
+
+ privateKeyImportButton := widget.NewButtonWithIcon("Import", icon.UploadOutlinedIconThemed, func() {
+ d := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) {
+ b, err := io.ReadAll(uc)
+ uc.Close()
+ if err != nil {
+ dialog.NewError(err, w).Show()
+ return
+ }
+ sk, err := sshkey.ParseKey(b)
+ if err != nil {
+ dialog.NewError(err, w).Show()
+ return
+ }
+ privateKeyEntryBind.Set(string(sk.PrivateKey()))
+ publicKeyEntryBind.Set(string(sk.PublicKey()))
+ fingerprintEntryBind.Set(string(sk.Fingerprint()))
+ }, w)
+ d.Show()
+ })
+
+ privateKeyExportButton := widget.NewButtonWithIcon("Export", icon.DownloadOutlinedIconThemed, func() {
+ d := dialog.NewFileSave(func(uc fyne.URIWriteCloser, err error) {
+ if uc == nil {
+ // file open dialog has been cancelled
+ return
+ }
+ defer uc.Close()
+ v, err := privateKeyEntryBind.Get()
+ if err != nil {
+ dialog.NewError(err, w).Show()
+ return
+ }
+ uc.Write([]byte(v))
+ }, w)
+ filename, _ := titleEntryBind.Get()
+ d.SetFileName(filename)
+ d.Show()
+ })
+
+ noteEntry := widget.NewEntryWithData(binding.BindString(&sshKeyItem.Note.Value))
+ noteEntry.MultiLine = true
+ noteEntry.Validator = nil
+
+ form := container.New(layout.NewFormLayout())
+ form.Add(widget.NewIcon(sh.Icon()))
+ form.Add(titleEntry)
+
+ form.Add(labelWithStyle("Private Key"))
+ form.Add(container.NewBorder(nil, nil, nil, container.NewVBox(privateKeyCopyButton, privateKeyMakeButton, privateKeyImportButton, privateKeyExportButton), privateKeyEntry))
+
+ form.Add(labelWithStyle("Public Key"))
+ form.Add(container.NewBorder(nil, nil, nil, container.NewVBox(publicKeyCopyButton, publicKeyExportButton), publicKeyEntry))
+
+ form.Add(labelWithStyle("Fingerprint"))
+ form.Add(fingerprintEntry)
+
+ form.Add(labelWithStyle("Note"))
+ form.Add(noteEntry)
+
+ return form, sshKeyItem
+}
+
+func (sh *SSHKey) Show(ctx context.Context, w fyne.Window) fyne.CanvasObject {
+ obj := titleRow(sh.Icon(), sh.Name)
+ obj = append(obj, rowWithAction("Private Key", sh.PrivateKey, rowActions{copy: true, ellipsis: 64, export: sh.Name}, w)...)
+ obj = append(obj, rowWithAction("Public Key", sh.PrivateKey, rowActions{copy: true, ellipsis: 64, export: sh.Name + ".pub"}, w)...)
+ obj = append(obj, copiableRow("Fingerprint", sh.Fingerprint, w)...)
+ obj = append(obj, copiableRow("Note", sh.Note.Value, w)...)
+ return container.New(layout.NewFormLayout(), obj...)
+}
diff --git a/internal/ui/vault_view.go b/internal/ui/vault_view.go
index 5361b18..c3be6fc 100644
--- a/internal/ui/vault_view.go
+++ b/internal/ui/vault_view.go
@@ -66,7 +66,28 @@ func newVaultView(mw *mainView, vault *paw.Vault) *vaultView {
vw.itemsWidget = newItemsWidget(vw.vault, vw.filterOptions)
vw.itemsWidget.OnSelected = func(meta *paw.Metadata) {
- item, _ := vw.mainView.storage.LoadItem(vw.vault, meta)
+ item, err := vw.mainView.storage.LoadItem(vw.vault, meta)
+ if err != nil {
+ msg := fmt.Sprintf("error loading %q.\nDo you want delete from the vault?", meta.Name)
+ fyne.LogError("error loading item from vault", err)
+ dialog.NewConfirm(
+ "Error",
+ msg,
+ func(delete bool) {
+ if delete {
+ item, _ = paw.NewItem(meta.Name, meta.Type)
+ vw.vault.DeleteItem(item) // remove item from vault
+ vw.mainView.storage.DeleteItem(vw.vault, item) // remove item from storage
+ vw.mainView.storage.StoreVault(vw.vault) // ensure vault is up-to-date
+ vw.itemsWidget.Reload(nil, vw.filterOptions)
+ vw.setContent(vw.defaultContent())
+ vw.Reload()
+ }
+ },
+ vw.mainView,
+ ).Show()
+ return
+ }
vw.setContentItem(NewFyneItem(item), vw.itemView)
}
vw.typeSelectEntry = vw.makeTypeSelectEntry()
@@ -247,11 +268,13 @@ func (vw *vaultView) makeItems() []paw.Item {
Hash: paw.TOTPHash(TOTPHash()),
Interval: TOTPInverval(),
}
+ sshkey := paw.NewSSHKey()
return []paw.Item{
note,
password,
website,
+ sshkey,
}
}
@@ -486,7 +509,7 @@ func (vw *vaultView) auditPasswordView() fyne.CanvasObject {
defer modal.Hide()
err := g.Wait()
if err != nil || errors.Is(ctx.Err(), context.Canceled) {
- ShowErrorDialog("Error auditing items", err, vw.mainView)
+ dialog.ShowError(err, vw.mainView)
return
}
@@ -583,7 +606,7 @@ func (vw *vaultView) importFromFile() {
err := json.NewDecoder(uc).Decode(&data)
if err != nil {
modal.Hide()
- ShowErrorDialog("Error importing items", err, vw.mainView)
+ dialog.ShowError(err, vw.mainView)
return
}
@@ -618,7 +641,7 @@ func (vw *vaultView) importFromFile() {
err = g.Wait()
if err != nil || errors.Is(ctx.Err(), context.Canceled) {
rollback(vw.vault, processed)
- ShowErrorDialog("Error importing items", err, vw.mainView)
+ dialog.ShowError(err, vw.mainView)
return
}
@@ -628,7 +651,7 @@ func (vw *vaultView) importFromFile() {
err = vw.mainView.storage.StoreVault(vw.vault)
if err != nil {
rollback(vw.vault, processed)
- ShowErrorDialog("Error importing items", err, vw.mainView)
+ dialog.ShowError(err, vw.mainView)
return
}
vw.itemsWidget.Reload(nil, vw.filterOptions)
@@ -713,13 +736,13 @@ func (vw *vaultView) exportToFile() {
defer modal.Hide()
err := g.Wait()
if err != nil || errors.Is(ctx.Err(), context.Canceled) {
- ShowErrorDialog("Error exporting items", err, vw.mainView)
+ dialog.ShowError(err, vw.mainView)
return
}
err = json.NewEncoder(uc).Encode(data)
if err != nil {
- ShowErrorDialog("Error exporting items", err, vw.mainView)
+ dialog.ShowError(err, vw.mainView)
}
}()
modal.Show()