Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
225
native/desktop/maplefile/internal/app/app_password.go
Normal file
225
native/desktop/maplefile/internal/app/app_password.go
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue