summaryrefslogtreecommitdiffstats
path: root/addressbook/addressbook.go
diff options
context:
space:
mode:
Diffstat (limited to 'addressbook/addressbook.go')
-rw-r--r--addressbook/addressbook.go217
1 files changed, 217 insertions, 0 deletions
diff --git a/addressbook/addressbook.go b/addressbook/addressbook.go
new file mode 100644
index 0000000..e6876c0
--- /dev/null
+++ b/addressbook/addressbook.go
@@ -0,0 +1,217 @@
+// Package addressbook manages contacts and their public keys
+package addressbook
+
+import (
+ "crypto/ed25519"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "os"
+ "sync"
+ "time"
+)
+
+// Contact represents a contact in the address book
+type Contact struct {
+ Name string `json:"name"`
+ PublicKey string `json:"public_key"` // hex-encoded Ed25519 public key
+ OnionAddress string `json:"onion_address"` // Tor hidden service address
+ Fingerprint string `json:"fingerprint"` // Short fingerprint for verification
+ AddedAt time.Time `json:"added_at"`
+ LastSeen time.Time `json:"last_seen"`
+ Verified bool `json:"verified"` // Manual verification flag
+ Notes string `json:"notes"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+}
+
+// AddressBook manages a collection of contacts
+type AddressBook struct {
+ contacts map[string]*Contact // key = public key hex
+ mu sync.RWMutex
+ path string
+}
+
+// NewAddressBook creates a new empty address book
+func NewAddressBook() *AddressBook {
+ return &AddressBook{
+ contacts: make(map[string]*Contact),
+ }
+}
+
+// LoadAddressBook loads an address book from disk
+func LoadAddressBook(path string) (*AddressBook, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read address book: %w", err)
+ }
+
+ var contacts []*Contact
+ if err := json.Unmarshal(data, &contacts); err != nil {
+ return nil, fmt.Errorf("failed to parse address book: %w", err)
+ }
+
+ ab := &AddressBook{
+ contacts: make(map[string]*Contact),
+ path: path,
+ }
+
+ for _, contact := range contacts {
+ ab.contacts[contact.PublicKey] = contact
+ }
+
+ return ab, nil
+}
+
+// Save saves the address book to disk
+func (ab *AddressBook) Save(path string) error {
+ ab.mu.RLock()
+ defer ab.mu.RUnlock()
+
+ // Convert map to slice
+ contacts := make([]*Contact, 0, len(ab.contacts))
+ for _, contact := range ab.contacts {
+ contacts = append(contacts, contact)
+ }
+
+ // Marshal to JSON
+ data, err := json.MarshalIndent(contacts, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to marshal address book: %w", err)
+ }
+
+ // Write to file
+ if err := os.WriteFile(path, data, 0600); err != nil {
+ return fmt.Errorf("failed to write address book: %w", err)
+ }
+
+ ab.path = path
+ return nil
+}
+
+// AddContact adds a new contact to the address book
+func (ab *AddressBook) AddContact(contact *Contact) error {
+ ab.mu.Lock()
+ defer ab.mu.Unlock()
+
+ // Validate public key
+ pubKeyBytes, err := hex.DecodeString(contact.PublicKey)
+ if err != nil {
+ return fmt.Errorf("invalid public key hex: %w", err)
+ }
+ if len(pubKeyBytes) != ed25519.PublicKeySize {
+ return fmt.Errorf("invalid public key size: expected %d, got %d", ed25519.PublicKeySize, len(pubKeyBytes))
+ }
+
+ // Generate fingerprint (first 8 bytes of public key)
+ contact.Fingerprint = hex.EncodeToString(pubKeyBytes[:8])
+ contact.AddedAt = time.Now()
+ contact.LastSeen = time.Now()
+
+ ab.contacts[contact.PublicKey] = contact
+ return nil
+}
+
+// GetContact retrieves a contact by public key
+func (ab *AddressBook) GetContact(publicKey string) (*Contact, bool) {
+ ab.mu.RLock()
+ defer ab.mu.RUnlock()
+
+ contact, exists := ab.contacts[publicKey]
+ return contact, exists
+}
+
+// GetContactByName retrieves a contact by name (first match)
+func (ab *AddressBook) GetContactByName(name string) (*Contact, bool) {
+ ab.mu.RLock()
+ defer ab.mu.RUnlock()
+
+ for _, contact := range ab.contacts {
+ if contact.Name == name {
+ return contact, true
+ }
+ }
+ return nil, false
+}
+
+// RemoveContact removes a contact from the address book
+func (ab *AddressBook) RemoveContact(publicKey string) bool {
+ ab.mu.Lock()
+ defer ab.mu.Unlock()
+
+ if _, exists := ab.contacts[publicKey]; exists {
+ delete(ab.contacts, publicKey)
+ return true
+ }
+ return false
+}
+
+// UpdateLastSeen updates the last seen timestamp for a contact
+func (ab *AddressBook) UpdateLastSeen(publicKey string) {
+ ab.mu.Lock()
+ defer ab.mu.Unlock()
+
+ if contact, exists := ab.contacts[publicKey]; exists {
+ contact.LastSeen = time.Now()
+ }
+}
+
+// MarkVerified marks a contact as manually verified
+func (ab *AddressBook) MarkVerified(publicKey string, verified bool) error {
+ ab.mu.Lock()
+ defer ab.mu.Unlock()
+
+ contact, exists := ab.contacts[publicKey]
+ if !exists {
+ return fmt.Errorf("contact not found")
+ }
+
+ contact.Verified = verified
+ return nil
+}
+
+// ListContacts returns all contacts
+func (ab *AddressBook) ListContacts() []*Contact {
+ ab.mu.RLock()
+ defer ab.mu.RUnlock()
+
+ contacts := make([]*Contact, 0, len(ab.contacts))
+ for _, contact := range ab.contacts {
+ contacts = append(contacts, contact)
+ }
+ return contacts
+}
+
+// Count returns the number of contacts
+func (ab *AddressBook) Count() int {
+ ab.mu.RLock()
+ defer ab.mu.RUnlock()
+ return len(ab.contacts)
+}
+
+// Clear removes all contacts
+func (ab *AddressBook) Clear() {
+ ab.mu.Lock()
+ defer ab.mu.Unlock()
+ ab.contacts = make(map[string]*Contact)
+}
+
+// VerifyFingerprint checks if a fingerprint matches a public key
+func VerifyFingerprint(publicKeyHex, fingerprint string) bool {
+ pubKeyBytes, err := hex.DecodeString(publicKeyHex)
+ if err != nil {
+ return false
+ }
+ if len(pubKeyBytes) < 8 {
+ return false
+ }
+
+ expectedFingerprint := hex.EncodeToString(pubKeyBytes[:8])
+ return expectedFingerprint == fingerprint
+}
+
+// String returns a string representation
+func (ab *AddressBook) String() string {
+ ab.mu.RLock()
+ defer ab.mu.RUnlock()
+ return fmt.Sprintf("AddressBook(contacts=%d)", len(ab.contacts))
+}