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) }