monorepo/native/desktop/maplefile/internal/app/app_password.go

225 lines
8.2 KiB
Go

package app
import (
"encoding/base64"
"fmt"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/maplefile/e2ee"
"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/utils"
)
// VerifyPassword verifies a password against stored encrypted data
func (a *Application) VerifyPassword(password string) (bool, error) {
// Get current session with encrypted data
session, err := a.authService.GetCurrentSession(a.ctx)
if err != nil || session == nil {
return false, fmt.Errorf("no active session")
}
// Check if we have the encrypted data needed for verification
if session.Salt == "" || session.EncryptedMasterKey == "" {
return false, fmt.Errorf("session missing encrypted data for password verification")
}
// Decode base64 inputs
salt, err := base64.StdEncoding.DecodeString(session.Salt)
if err != nil {
a.logger.Error("Failed to decode salt", zap.Error(err))
return false, fmt.Errorf("invalid salt encoding")
}
encryptedMasterKeyBytes, err := base64.StdEncoding.DecodeString(session.EncryptedMasterKey)
if err != nil {
a.logger.Error("Failed to decode encrypted master key", zap.Error(err))
return false, fmt.Errorf("invalid master key encoding")
}
// Determine which KDF algorithm to use
kdfAlgorithm := session.KDFAlgorithm
if kdfAlgorithm == "" {
kdfAlgorithm = e2ee.PBKDF2Algorithm
}
// Try to derive KEK and decrypt master key using SecureKeyChain
// If decryption succeeds, password is correct
keychain, err := e2ee.NewSecureKeyChainWithAlgorithm(password, salt, kdfAlgorithm)
if err != nil {
a.logger.Debug("Password verification failed - could not derive key", zap.String("email", utils.MaskEmail(session.Email)))
return false, nil // Password is incorrect, but not an error condition
}
defer keychain.Clear()
// Split nonce and ciphertext from encrypted master key
// Use auto-detection to handle both ChaCha20 (12-byte nonce) and XSalsa20 (24-byte nonce)
masterKeyNonce, masterKeyCiphertext, err := e2ee.SplitNonceAndCiphertextAuto(encryptedMasterKeyBytes)
if err != nil {
a.logger.Error("Failed to split encrypted master key", zap.Error(err))
return false, fmt.Errorf("invalid master key format")
}
encryptedMasterKeyStruct := &e2ee.EncryptedKey{
Ciphertext: masterKeyCiphertext,
Nonce: masterKeyNonce,
}
// Try to decrypt the master key into protected memory
masterKey, err := keychain.DecryptMasterKeySecure(encryptedMasterKeyStruct)
if err != nil {
a.logger.Debug("Password verification failed - incorrect password", zap.String("email", utils.MaskEmail(session.Email)))
return false, nil // Password is incorrect, but not an error condition
}
// Copy master key bytes before destroying the buffer
// We'll cache it after verification succeeds
masterKeyBytes := make([]byte, masterKey.Size())
copy(masterKeyBytes, masterKey.Bytes())
masterKey.Destroy()
// Cache the master key for the session (already decrypted, no need to re-derive)
if err := a.keyCache.StoreMasterKey(session.Email, masterKeyBytes); err != nil {
a.logger.Warn("Failed to cache master key during password verification", zap.Error(err))
// Don't fail verification if caching fails
} else {
a.logger.Info("Master key cached successfully during password verification", zap.String("email", utils.MaskEmail(session.Email)))
}
a.logger.Info("Password verified successfully", zap.String("email", utils.MaskEmail(session.Email)))
return true, nil
}
// StorePasswordForSession stores password for current session (used by PasswordPrompt)
func (a *Application) StorePasswordForSession(password string) error {
session, err := a.authService.GetCurrentSession(a.ctx)
if err != nil || session == nil {
return fmt.Errorf("no active session")
}
if err := a.passwordStore.StorePassword(session.Email, password); err != nil {
a.logger.Error("Failed to store password for session", zap.String("email", utils.MaskEmail(session.Email)), zap.Error(err))
return err
}
a.logger.Info("Password re-stored in secure RAM after app restart", zap.String("email", utils.MaskEmail(session.Email)))
// Note: Master key caching is now handled in VerifyPassword()
// to avoid running PBKDF2 twice. The password verification step
// already derives KEK and decrypts the master key, so we cache it there.
// This eliminates redundant key derivation delay.
return nil
}
// GetStoredPassword retrieves the stored password for current session
func (a *Application) GetStoredPassword() (string, error) {
session, err := a.authService.GetCurrentSession(a.ctx)
if err != nil || session == nil {
return "", fmt.Errorf("no active session")
}
return a.passwordStore.GetPassword(session.Email)
}
// HasStoredPassword checks if password is stored for current session
func (a *Application) HasStoredPassword() bool {
session, err := a.authService.GetCurrentSession(a.ctx)
if err != nil || session == nil {
return false
}
return a.passwordStore.HasPassword(session.Email)
}
// ClearStoredPassword clears the stored password (optional, for security-sensitive operations)
func (a *Application) ClearStoredPassword() error {
session, err := a.authService.GetCurrentSession(a.ctx)
if err != nil || session == nil {
return fmt.Errorf("no active session")
}
return a.passwordStore.ClearPassword(session.Email)
}
// cacheMasterKeyFromPassword decrypts and caches the master key for the session
// This is an internal helper method used by CompleteLogin and StorePasswordForSession
func (a *Application) cacheMasterKeyFromPassword(email, password, saltBase64, encryptedMasterKeyBase64, kdfAlgorithm string) error {
// Default to PBKDF2-SHA256
if kdfAlgorithm == "" {
kdfAlgorithm = e2ee.PBKDF2Algorithm
}
// Decode base64 inputs
salt, err := base64.StdEncoding.DecodeString(saltBase64)
if err != nil {
return fmt.Errorf("invalid salt encoding: %w", err)
}
encryptedMasterKeyBytes, err := base64.StdEncoding.DecodeString(encryptedMasterKeyBase64)
if err != nil {
return fmt.Errorf("invalid master key encoding: %w", err)
}
// Create secure keychain to derive KEK using the correct KDF algorithm
keychain, err := e2ee.NewSecureKeyChainWithAlgorithm(password, salt, kdfAlgorithm)
if err != nil {
return fmt.Errorf("failed to derive KEK: %w", err)
}
defer keychain.Clear()
// Split nonce and ciphertext using 24-byte nonce (XSalsa20 secretbox format from web frontend)
masterKeyNonce, masterKeyCiphertext, err := e2ee.SplitNonceAndCiphertextSecretBox(encryptedMasterKeyBytes)
if err != nil {
return fmt.Errorf("invalid master key format: %w", err)
}
encryptedMasterKeyStruct := &e2ee.EncryptedKey{
Ciphertext: masterKeyCiphertext,
Nonce: masterKeyNonce,
}
// Decrypt master key into secure buffer (auto-detects cipher based on nonce size)
masterKey, err := keychain.DecryptMasterKeySecure(encryptedMasterKeyStruct)
if err != nil {
return fmt.Errorf("failed to decrypt master key: %w", err)
}
// CRITICAL: Copy bytes BEFORE destroying the buffer to avoid SIGBUS fault
// masterKey.Bytes() returns a pointer to LockedBuffer memory which becomes
// invalid after Destroy() is called
masterKeyBytes := make([]byte, masterKey.Size())
copy(masterKeyBytes, masterKey.Bytes())
// Now safely destroy the secure buffer
masterKey.Destroy()
// Store the copied bytes in cache
if err := a.keyCache.StoreMasterKey(email, masterKeyBytes); err != nil {
return fmt.Errorf("failed to cache master key: %w", err)
}
a.logger.Info("Master key cached successfully for session", zap.String("email", utils.MaskEmail(email)))
return nil
}
// GetCachedMasterKey retrieves the cached master key for the current session
// This is exported and can be called from frontend for file operations
// Returns the master key bytes and a cleanup function that MUST be called when done
func (a *Application) GetCachedMasterKey() ([]byte, func(), error) {
session, err := a.authService.GetCurrentSession(a.ctx)
if err != nil || session == nil {
return nil, nil, fmt.Errorf("no active session")
}
return a.keyCache.GetMasterKey(session.Email)
}
// HasCachedMasterKey checks if a master key is cached for the current session
func (a *Application) HasCachedMasterKey() bool {
session, err := a.authService.GetCurrentSession(a.ctx)
if err != nil || session == nil {
return false
}
return a.keyCache.HasMasterKey(session.Email)
}