/* * Copyright (c) 2026 Kaiy Ragur * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package account import ( "crypto/ed25519" "errors" "fmt" "log" "os" "strings" "github.com/neilalexander/yggmail/internal/storage" "github.com/neilalexander/yggmail/internal/storage/types" "github.com/neilalexander/yggmail/internal/utils" "golang.org/x/crypto/bcrypt" "golang.org/x/term" ) type Manager struct { storage storage.Storage log *log.Logger } type SecretProvider func() (string, error) // Function for wrapping by your Provider func DefaultProvider(msg string) (string, error) { fmt.Print(msg) p, err := term.ReadPassword(int(os.Stdin.Fd())) password := strings.TrimSpace(string(p)) fmt.Println() return password, err } func NewManager(storage storage.Storage, log *log.Logger) *Manager { return &Manager{storage: storage, log: log} } func (m *Manager) AccountList() ([]*types.Account, error) { return m.storage.AccountList() } func (m *Manager) CreateNewAccount(p SecretProvider) (*types.Account, error) { _, sk, err := ed25519.GenerateKey(nil) if err != nil { return nil, fmt.Errorf("account.Manager.CreateNewAccount: GenerateKey: %w", err) } acc, err := m.CreateAccount(sk, p) if err != nil { return nil, fmt.Errorf("account.Manager.CreateNewAccount: %w", err) } return acc, nil } func (m *Manager) CreateAccount(sk ed25519.PrivateKey, p SecretProvider) (*types.Account, error) { if len(sk) != ed25519.PrivateKeySize { return nil, fmt.Errorf("account.Manager.CreateAccount: PrivateKeySize != 32") } password, err := p() if err != nil { return nil, fmt.Errorf("account.Manger.CreateAccount: SecretProvider: %w", err) } pk := sk.Public().(ed25519.PublicKey) enSk, err := utils.AESEncrypt(utils.EncodeString(sk), password) if err != nil { return nil, fmt.Errorf("account.Manger.CreateAccount: encrypt privateKey: %w", err) } passwdhash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return nil, fmt.Errorf("account.Manager.CreateAccount: hash password: %w", err) } // nulling sec-data sk = nil password = "" acc := &types.Account{ PublicKey: utils.EncodeString(pk), // TODO Added Aliases.__. Aliases: "", PasswdHash: string(passwdhash), Privatekey: enSk, } if err := m.storage.AccountCreate(acc); err != nil { return nil, fmt.Errorf("account.Manager.CreateAccount: storage create account: %w", err) } for _, folder := range []string{"INBOX", "Outbox", "Sent", "Trash", "Drafts"} { m.storage.MailboxCreate(acc.PublicKey, folder) } return acc, nil } func (m *Manager) AddAccount(pubkey string) (*types.Account, error) { pkb, err := utils.DecodeString(pubkey) if err != nil { return nil, fmt.Errorf("account.Manager.AddAccount: can't decode account key") } if len(pkb) != ed25519.PublicKeySize { return nil, fmt.Errorf("account.Manager.AddAccount: wrong key size") } acc := &types.Account{ PublicKey: utils.EncodeString(pkb), // TODO Added Aliases.__. Aliases: "", PasswdHash: "", Privatekey: "", } if err := m.storage.AccountCreate(acc); err != nil { return nil, fmt.Errorf("account.Manager.CreateAccount: storage create account: %w", err) } for _, folder := range []string{"INBOX", "Outbox", "Sent", "Trash", "Drafts"} { m.storage.MailboxCreate(acc.PublicKey, folder) } return acc, nil } func (m *Manager) GetAccount(pubkey string) (*types.Account, error) { acc, err := m.storage.AccountGet(pubkey) if err != nil { return nil, fmt.Errorf("failed to find: %w", err) } if acc == nil { return nil, fmt.Errorf("failed to find: wrong credentials : %w", err) // return nil, fmt.Errorf("failed to find: wrong username : %w", err) } return acc, nil } func (m *Manager) DeleteAccount(pubkey string) error { if err := m.storage.AccountDelete(pubkey); err != nil { return fmt.Errorf("account.Manager.DeleteAccount: %w", err) } return nil } var ErrInvalidCredentials = errors.New("Invalid credentials") func (m *Manager) Login(username, password string) (session *Session, err error) { _, aPk, err := utils.ParseAddress(username) if err != nil { return nil, fmt.Errorf("failed to authenticate: wrong domain or username") } // Trim away whitespace of UTF-8 bytes now as string password = strings.TrimSpace(password) // For security all errors is same. Uncomment for debug! acc, err := m.storage.AccountGet(utils.EncodeString(aPk)) if err != nil || acc == nil { // return nil, fmt.Errorf("failed to authenticate: storage: %w", err) return nil, ErrInvalidCredentials } if err := bcrypt.CompareHashAndPassword([]byte(acc.PasswdHash), []byte(password)); err != nil { // return nil, fmt.Errorf("failed to authenticate: wrong password : %w", err) return nil, ErrInvalidCredentials } // Autheticated! if acc.PasswdHash == "" || acc.Privatekey == "" { return nil, fmt.Errorf("failed have not account.PrivateKey for client mode!") } aSkStr, err := utils.AESDecrypt(acc.Privatekey, password) if err != nil { return nil, fmt.Errorf("failed to authenticate: Privkey AESDecrypt: %w", err) } password = "" aSk, err := utils.DecodeString(aSkStr) if err != nil { return nil, fmt.Errorf("failed to authenticate: Privkey Parse: %w", err) } aSkStr = "" return NewSession(acc, ed25519.PrivateKey(aSk)), nil } func (m *Manager) ChangePassword(pubkey string) { panic("Not Implemented") }