225 lines
8.2 KiB
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)
|
|
}
|