traygent/agent.go

408 lines
7.7 KiB
Go

package main
import (
"bytes"
"crypto/rand"
"crypto/subtle"
"errors"
"fmt"
"log"
"net"
"sync"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
var errLocked = errors.New("agent is locked")
const expFormat = "Mon Jan 2 15:04:05 MST 2006"
type privKey struct {
signer ssh.Signer
comment string
expire *time.Time
pubKey ssh.PublicKey
fingerPrint string
}
func NewPrivKey(signer ssh.Signer, key agent.AddedKey) privKey {
pub := signer.PublicKey()
pk := privKey{
signer: signer,
comment: key.Comment,
pubKey: pub,
fingerPrint: ssh.FingerprintSHA256(pub),
}
pk.setExpire(key)
return pk
}
func (p *privKey) String() string {
pk := p.signer.PublicKey()
return fmt.Sprintf("%s %s %s %s",
pk.Type(),
p.fingerPrint,
p.comment,
p.expire.Format(expFormat),
)
}
func (p *privKey) GetType() string {
return p.pubKey.Type()
}
func (p *privKey) GetSum() string {
return p.fingerPrint
}
func (p *privKey) GetComment() string {
return p.comment
}
func (p *privKey) setExpire(key agent.AddedKey) {
exp := key.LifetimeSecs
if exp <= 0 {
exp = 300
}
t := time.Now().Add(time.Duration(exp) * time.Second)
p.expire = &t
}
// Traygent extends x/crypto/ssh/agent to hook into fyne for various tasks:
// - notifications
// - allowing UI elements to represent keys
type Traygent struct {
app fyne.App
window fyne.Window
keyList *widget.Table
desk desktop.App
expire uint32
listener net.Listener
mu sync.RWMutex
keys []privKey
locked bool
passphrase []byte
}
func NewTraygent() agent.Agent {
return &Traygent{
expire: 360,
}
}
func (t *Traygent) log(title, msgFmt string, msg ...any) {
fmt.Println("log")
msgStr := fmt.Sprintf(msgFmt, msg...)
log.Println(msgStr)
// TODO: fyne can't send vanishing notifications..
//notif := fyne.NewNotification(title, msgStr)
//t.app.SendNotification(notif)
}
func (t *Traygent) remove(key ssh.PublicKey) error {
fmt.Println("remove")
hasKey := false
for i := 0; i < len(t.keys); {
if bytes.Equal(
t.keys[i].signer.PublicKey().Marshal(),
key.Marshal(),
) {
hasKey = true
t.keys[i] = t.keys[len(t.keys)-1]
t.keys = t.keys[:len(t.keys)-1]
t.log("Key removed", "removed key: %q\n", ssh.FingerprintSHA256(key))
continue
} else {
i++
}
}
if !hasKey {
return errors.New("key not found")
}
return nil
}
func (t *Traygent) RemoveLocked() {
fmt.Println("RemoveLocked")
t.mu.Lock()
defer t.mu.Unlock()
for _, k := range t.keys {
if k.expire != nil && time.Now().After(*k.expire) {
t.remove(k.signer.PublicKey())
}
}
}
func (t *Traygent) List() ([]*agent.Key, error) {
fmt.Println("List")
t.mu.RLock()
defer t.mu.RUnlock()
var pubKeys []*agent.Key
if t.locked {
return nil, nil
}
for _, k := range t.keys {
pubKeys = append(pubKeys, &agent.Key{
Blob: k.pubKey.Marshal(),
Comment: fmt.Sprintf("%s [%s]", k.comment, k.expire.Format(expFormat)),
Format: k.pubKey.Type(),
})
}
return pubKeys, nil
}
func (t *Traygent) passphrasePrompt(isLock bool, doneFunc func([]byte)) {
fmt.Println("passphrasePrompt")
btnStr := "Unlock"
titleStr := "Unlock Agent"
if isLock {
btnStr = "Lock"
titleStr = "Lock Agent"
}
passphrase := widget.NewPasswordEntry()
items := []*widget.FormItem{
widget.NewFormItem("Passphrase", passphrase),
}
dialog.ShowForm(titleStr, btnStr, "Cancel", items, func(b bool) {
if !b {
return
}
doneFunc([]byte(passphrase.Text))
}, t.window)
}
func (t *Traygent) Lock(unused []byte) error {
fmt.Println("Lock")
t.log("Agent locked", "locking agent")
if t.locked {
return errLocked
}
t.passphrasePrompt(true, func(passphrase []byte) {
t.mu.Lock()
defer t.mu.Unlock()
t.locked = true
t.passphrase = passphrase
t.Resize()
})
return nil
}
func (t *Traygent) Unlock(unused []byte) error {
fmt.Println("Unlock")
log.Println("unlocking agent")
if !t.locked {
return errors.New("not locked")
}
t.passphrasePrompt(true, func(passphrase []byte) {
t.mu.Lock()
defer t.mu.Unlock()
log.Println("hur")
if subtle.ConstantTimeCompare(passphrase, t.passphrase) == 1 {
t.locked = false
t.passphrase = nil
t.Resize()
}
})
return nil
}
func (t *Traygent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
fmt.Println("Sign")
return t.SignWithFlags(key, data, 0)
}
func (t *Traygent) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) {
fmt.Println("SignWithFlags")
if t.locked {
return nil, errLocked
}
t.RemoveLocked()
t.mu.Lock()
defer t.mu.Unlock()
pk := key.Marshal()
for _, k := range t.keys {
if bytes.Equal(k.signer.PublicKey().Marshal(), pk) {
if flags == 0 {
return k.signer.Sign(rand.Reader, data)
} else {
if algSiger, ok := k.signer.(ssh.AlgorithmSigner); !ok {
return nil, fmt.Errorf("%T is not supported", k.signer)
} else {
var alg string
switch flags {
case agent.SignatureFlagRsaSha256:
alg = ssh.KeyAlgoRSASHA256
case agent.SignatureFlagRsaSha512:
alg = ssh.KeyAlgoRSASHA512
default:
return nil, fmt.Errorf("unsupported signature flags: %d", flags)
}
return algSiger.SignWithAlgorithm(rand.Reader, data, alg)
}
}
}
}
return nil, errors.New("not found")
}
func (t *Traygent) Signers() ([]ssh.Signer, error) {
fmt.Println("Signers")
log.Println("signers")
if t.locked {
return nil, errLocked
}
t.RemoveLocked()
t.mu.Lock()
defer t.mu.Unlock()
signers := make([]ssh.Signer, 0, len(t.keys))
for _, k := range t.keys {
signers = append(signers, k.signer)
}
return signers, nil
}
func (t *Traygent) getMaxes() (string, string, string, string) {
fmt.Println("getMaxes")
t.mu.RLock()
defer t.mu.RUnlock()
maxType := ""
maxSum := ""
maxComment := ""
for _, entry := range t.keys {
if len(entry.GetType()) > len(maxType) {
maxType = entry.GetType()
}
if len(entry.GetSum()) > len(maxSum) {
maxSum = entry.GetSum()
}
if len(entry.GetComment()) > len(maxComment) {
maxComment = entry.GetComment()
}
}
return maxType, maxSum, maxComment, expFormat
}
func (t *Traygent) Resize() {
fmt.Println("Resize")
t.mu.RLock()
defer t.mu.RUnlock()
maxType, maxFP, maxCmt, maxExp := t.getMaxes()
typeSize := fyne.MeasureText(maxType, theme.TextSize()+2, fyne.TextStyle{})
fpSize := fyne.MeasureText(maxFP, theme.TextSize()+2, fyne.TextStyle{})
cmtSize := fyne.MeasureText(maxCmt, theme.TextSize()+2, fyne.TextStyle{})
expSize := fyne.MeasureText(maxExp, theme.TextSize()+2, fyne.TextStyle{})
t.keyList.SetColumnWidth(0, typeSize.Width)
t.keyList.SetColumnWidth(1, fpSize.Width)
t.keyList.SetColumnWidth(2, cmtSize.Width)
t.keyList.SetColumnWidth(3, expSize.Width)
iconImg := buildImage(len(t.keys), t.locked)
t.desk.SetSystemTrayIcon(iconImg)
}
func (t *Traygent) Add(key agent.AddedKey) error {
fmt.Println("Add")
signer, err := ssh.NewSignerFromKey(key.PrivateKey)
if err != nil {
return err
}
t.mu.Lock()
p := NewPrivKey(signer, key)
t.keys = append(t.keys, p)
t.log("Key added", "added %q to agent", p.fingerPrint)
t.mu.Unlock()
t.Resize()
return nil
}
func (t *Traygent) RemoveAll() error {
fmt.Println("RemoveAll")
if t.locked {
return errLocked
}
t.mu.Lock()
t.keys = nil
t.log("All keys removed", "removed all keys from agent")
t.mu.Unlock()
t.Resize()
return nil
}
func (t *Traygent) Remove(key ssh.PublicKey) error {
fmt.Println("Remove")
if t.locked {
return errLocked
}
t.mu.Lock()
err := t.remove(key)
t.log("Key removed", "remove key from agent")
t.mu.Unlock()
t.Resize()
return err
}