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
977
native/desktop/maplefile/internal/app/app_auth.go
Normal file
977
native/desktop/maplefile/internal/app/app_auth.go
Normal file
|
|
@ -0,0 +1,977 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/tyler-smith/go-bip39"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/maplefile/client"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/maplefile/e2ee"
|
||||
"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/service/inputvalidation"
|
||||
"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/service/ratelimiter"
|
||||
"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/utils"
|
||||
)
|
||||
|
||||
// RequestOTT requests a one-time token for login
|
||||
func (a *Application) RequestOTT(email string) error {
|
||||
// Validate input
|
||||
if err := inputvalidation.ValidateEmail(email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check rate limit before making request
|
||||
// Note: We do NOT reset on success here - the rate limit prevents spamming
|
||||
// the "request OTT" button. Users should wait between OTT requests.
|
||||
if err := a.rateLimiter.Check(ratelimiter.OpRequestOTT, email); err != nil {
|
||||
a.logger.Warn("OTT request rate limited",
|
||||
zap.String("email", utils.MaskEmail(email)),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return a.authService.RequestOTT(a.ctx, email)
|
||||
}
|
||||
|
||||
// Logout logs out the current user and deletes all local data (default behavior for security).
|
||||
// Use LogoutWithOptions for more control over local data deletion.
|
||||
func (a *Application) Logout() error {
|
||||
return a.LogoutWithOptions(true) // Default to deleting local data for security
|
||||
}
|
||||
|
||||
// LogoutWithOptions logs out the current user with control over local data deletion.
|
||||
// If deleteLocalData is true, all locally cached files and metadata will be permanently deleted.
|
||||
// If deleteLocalData is false, local data is preserved for faster login next time.
|
||||
func (a *Application) LogoutWithOptions(deleteLocalData bool) error {
|
||||
// Get session before clearing
|
||||
session, _ := a.authService.GetCurrentSession(a.ctx)
|
||||
var userEmail string
|
||||
if session != nil {
|
||||
userEmail = session.Email
|
||||
}
|
||||
|
||||
// Stop token manager first
|
||||
stopCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := a.tokenManager.Stop(stopCtx); err != nil {
|
||||
a.logger.Error("Failed to stop token manager during logout", zap.Error(err))
|
||||
// Continue with logout even if token manager stop failed
|
||||
}
|
||||
|
||||
// Clear stored password from RAM
|
||||
if session != nil {
|
||||
if err := a.passwordStore.ClearPassword(session.Email); err != nil {
|
||||
a.logger.Error("Failed to clear stored password", zap.Error(err))
|
||||
} else {
|
||||
a.logger.Info("Password cleared from secure RAM", zap.String("email", utils.MaskEmail(session.Email)))
|
||||
}
|
||||
|
||||
// Clear cached master key from memory (if it exists)
|
||||
if a.keyCache.HasMasterKey(session.Email) {
|
||||
if err := a.keyCache.ClearMasterKey(session.Email); err != nil {
|
||||
a.logger.Warn("Failed to clear cached master key", zap.Error(err))
|
||||
} else {
|
||||
a.logger.Info("Cached master key cleared from secure memory", zap.String("email", utils.MaskEmail(session.Email)))
|
||||
}
|
||||
} else {
|
||||
a.logger.Debug("No cached master key to clear (expected after app restart)", zap.String("email", utils.MaskEmail(session.Email)))
|
||||
}
|
||||
}
|
||||
|
||||
// Close search index
|
||||
if err := a.searchService.Close(); err != nil {
|
||||
a.logger.Error("Failed to close search index during logout", zap.Error(err))
|
||||
// Continue with logout even if search cleanup fails
|
||||
} else {
|
||||
a.logger.Info("Search index closed")
|
||||
}
|
||||
|
||||
// Handle local data based on user preference
|
||||
if deleteLocalData && userEmail != "" {
|
||||
// Delete all local data permanently
|
||||
if err := a.storageManager.DeleteUserData(userEmail); err != nil {
|
||||
a.logger.Error("Failed to delete local user data", zap.Error(err))
|
||||
// Continue with logout even if deletion fails
|
||||
} else {
|
||||
a.logger.Info("All local user data deleted", zap.String("email", utils.MaskEmail(userEmail)))
|
||||
}
|
||||
} else {
|
||||
// Just cleanup storage connections (keep data on disk)
|
||||
a.storageManager.Cleanup()
|
||||
a.logger.Info("User storage connections closed, local data preserved")
|
||||
}
|
||||
|
||||
// Clear session
|
||||
return a.authService.Logout(a.ctx)
|
||||
}
|
||||
|
||||
// GetLocalDataSize returns the size of locally stored data for the current user in bytes.
|
||||
// This can be used to show the user how much data will be deleted on logout.
|
||||
func (a *Application) GetLocalDataSize() (int64, error) {
|
||||
session, err := a.authService.GetCurrentSession(a.ctx)
|
||||
if err != nil || session == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
size, err := a.storageManager.GetUserDataSize(session.Email)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to get local data size", zap.Error(err))
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return size, nil
|
||||
}
|
||||
|
||||
// IsLoggedIn checks if a user is logged in
|
||||
func (a *Application) IsLoggedIn() (bool, error) {
|
||||
return a.authService.IsLoggedIn(a.ctx)
|
||||
}
|
||||
|
||||
// Register creates a new user account
|
||||
func (a *Application) Register(input *client.RegisterInput) error {
|
||||
// Validate input
|
||||
if err := inputvalidation.ValidateEmail(input.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := inputvalidation.ValidateDisplayName(input.FirstName, "first name"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := inputvalidation.ValidateDisplayName(input.LastName, "last name"); err != nil {
|
||||
return err
|
||||
}
|
||||
// Note: Password is not sent directly in RegisterInput - it's used client-side
|
||||
// to derive encryption keys. The encrypted master key and salt are validated
|
||||
// by their presence and format on the server side.
|
||||
|
||||
// Check rate limit before making request
|
||||
// Note: We do NOT reset on success - registration is a one-time operation
|
||||
// and keeping the rate limit prevents re-registration spam attempts.
|
||||
if err := a.rateLimiter.Check(ratelimiter.OpRegister, input.Email); err != nil {
|
||||
a.logger.Warn("Registration rate limited",
|
||||
zap.String("email", utils.MaskEmail(input.Email)),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return a.authService.Register(a.ctx, input)
|
||||
}
|
||||
|
||||
// VerifyEmail verifies the email with the verification code
|
||||
func (a *Application) VerifyEmail(email, code string) error {
|
||||
// Validate input
|
||||
if err := inputvalidation.ValidateEmail(email); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := inputvalidation.ValidateOTT(code); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check rate limit before making request
|
||||
if err := a.rateLimiter.Check(ratelimiter.OpVerifyEmail, email); err != nil {
|
||||
a.logger.Warn("Email verification rate limited",
|
||||
zap.String("email", utils.MaskEmail(email)),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
input := &client.VerifyEmailInput{
|
||||
Email: email,
|
||||
Code: code,
|
||||
}
|
||||
err := a.authService.VerifyEmail(a.ctx, input)
|
||||
if err == nil {
|
||||
// Reset rate limit on success
|
||||
a.rateLimiter.Reset(ratelimiter.OpVerifyEmail, email)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// VerifyOTTResponse contains the OTT verification response with encrypted challenge
|
||||
type VerifyOTTResponse struct {
|
||||
Message string `json:"message"`
|
||||
ChallengeID string `json:"challengeId"`
|
||||
EncryptedChallenge string `json:"encryptedChallenge"`
|
||||
Salt string `json:"salt"`
|
||||
EncryptedMasterKey string `json:"encryptedMasterKey"`
|
||||
EncryptedPrivateKey string `json:"encryptedPrivateKey"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
// KDFAlgorithm specifies which key derivation algorithm to use.
|
||||
// Value: "PBKDF2-SHA256"
|
||||
KDFAlgorithm string `json:"kdfAlgorithm"`
|
||||
}
|
||||
|
||||
// VerifyOTT verifies the one-time token and returns the encrypted challenge
|
||||
func (a *Application) VerifyOTT(email, ott string) (*VerifyOTTResponse, error) {
|
||||
// Validate input
|
||||
if err := inputvalidation.ValidateEmail(email); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := inputvalidation.ValidateOTT(ott); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check rate limit before making request
|
||||
if err := a.rateLimiter.Check(ratelimiter.OpVerifyOTT, email); err != nil {
|
||||
a.logger.Warn("OTT verification rate limited",
|
||||
zap.String("email", utils.MaskEmail(email)),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := a.authService.VerifyOTT(a.ctx, email, ott)
|
||||
if err != nil {
|
||||
a.logger.Error("OTT verification failed", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reset rate limit on success
|
||||
a.rateLimiter.Reset(ratelimiter.OpVerifyOTT, email)
|
||||
|
||||
// Get KDF algorithm from response, default to PBKDF2-SHA256
|
||||
kdfAlgorithm := resp.KDFAlgorithm
|
||||
if kdfAlgorithm == "" {
|
||||
kdfAlgorithm = e2ee.PBKDF2Algorithm
|
||||
}
|
||||
|
||||
return &VerifyOTTResponse{
|
||||
Message: resp.Message,
|
||||
ChallengeID: resp.ChallengeID,
|
||||
EncryptedChallenge: resp.EncryptedChallenge,
|
||||
Salt: resp.Salt,
|
||||
EncryptedMasterKey: resp.EncryptedMasterKey,
|
||||
EncryptedPrivateKey: resp.EncryptedPrivateKey,
|
||||
PublicKey: resp.PublicKey,
|
||||
KDFAlgorithm: kdfAlgorithm,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CompleteLoginInput contains the data needed to complete login
|
||||
type CompleteLoginInput struct {
|
||||
Email string `json:"email"`
|
||||
ChallengeID string `json:"challengeId"`
|
||||
DecryptedData string `json:"decryptedData"`
|
||||
Password string `json:"password"`
|
||||
|
||||
// Encrypted user data for future password verification
|
||||
Salt string `json:"salt"`
|
||||
EncryptedMasterKey string `json:"encryptedMasterKey"`
|
||||
EncryptedPrivateKey string `json:"encryptedPrivateKey"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
|
||||
// KDFAlgorithm specifies which key derivation algorithm to use.
|
||||
// Value: "PBKDF2-SHA256"
|
||||
KDFAlgorithm string `json:"kdfAlgorithm"`
|
||||
}
|
||||
|
||||
// CompleteLogin completes the login process with the decrypted challenge
|
||||
func (a *Application) CompleteLogin(input *CompleteLoginInput) error {
|
||||
// Validate input
|
||||
if err := inputvalidation.ValidateEmail(input.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := inputvalidation.ValidatePassword(input.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
if input.ChallengeID == "" {
|
||||
return fmt.Errorf("challenge ID is required")
|
||||
}
|
||||
if input.DecryptedData == "" {
|
||||
return fmt.Errorf("decrypted data is required")
|
||||
}
|
||||
|
||||
// Check rate limit before making request
|
||||
if err := a.rateLimiter.Check(ratelimiter.OpCompleteLogin, input.Email); err != nil {
|
||||
a.logger.Warn("Login completion rate limited",
|
||||
zap.String("email", utils.MaskEmail(input.Email)),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
clientInput := &client.CompleteLoginInput{
|
||||
Email: input.Email,
|
||||
ChallengeID: input.ChallengeID,
|
||||
DecryptedData: input.DecryptedData,
|
||||
}
|
||||
|
||||
_, err := a.authService.CompleteLogin(a.ctx, clientInput)
|
||||
if err != nil {
|
||||
a.logger.Error("Login completion failed", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset all rate limits for this user on successful login
|
||||
a.rateLimiter.ResetAll(input.Email)
|
||||
|
||||
// Store encrypted user data in session for future password verification
|
||||
session, err := a.authService.GetCurrentSession(a.ctx)
|
||||
if err == nil && session != nil {
|
||||
session.Salt = input.Salt
|
||||
session.EncryptedMasterKey = input.EncryptedMasterKey
|
||||
session.EncryptedPrivateKey = input.EncryptedPrivateKey
|
||||
session.PublicKey = input.PublicKey
|
||||
// Store KDF algorithm so VerifyPassword knows which algorithm to use
|
||||
session.KDFAlgorithm = input.KDFAlgorithm
|
||||
if session.KDFAlgorithm == "" {
|
||||
session.KDFAlgorithm = e2ee.PBKDF2Algorithm
|
||||
}
|
||||
|
||||
// Update session with encrypted data
|
||||
if err := a.authService.UpdateSession(a.ctx, session); err != nil {
|
||||
a.logger.Warn("Failed to update session with encrypted data", zap.Error(err))
|
||||
// Continue anyway - password storage will still work
|
||||
} else {
|
||||
a.logger.Info("Encrypted user data stored in session for password verification")
|
||||
}
|
||||
}
|
||||
|
||||
// Store password in secure RAM
|
||||
if err := a.passwordStore.StorePassword(input.Email, input.Password); err != nil {
|
||||
a.logger.Error("Failed to store password in RAM", zap.Error(err))
|
||||
// Don't fail login if password storage fails
|
||||
} else {
|
||||
a.logger.Info("Password stored securely in RAM for E2EE operations", zap.String("email", utils.MaskEmail(input.Email)))
|
||||
}
|
||||
|
||||
// Cache master key for session to avoid re-decrypting for every file operation
|
||||
if input.Salt != "" && input.EncryptedMasterKey != "" && input.Password != "" {
|
||||
kdfAlgorithm := input.KDFAlgorithm
|
||||
if kdfAlgorithm == "" {
|
||||
kdfAlgorithm = e2ee.PBKDF2Algorithm
|
||||
}
|
||||
if err := a.cacheMasterKeyFromPassword(input.Email, input.Password, input.Salt, input.EncryptedMasterKey, kdfAlgorithm); err != nil {
|
||||
a.logger.Warn("Failed to cache master key during login", zap.Error(err))
|
||||
// Continue anyway - user can still use the app, just slower
|
||||
}
|
||||
}
|
||||
|
||||
a.logger.Info("User logged in successfully", zap.String("email", utils.MaskEmail(input.Email)))
|
||||
|
||||
// Initialize user-specific storage for the logged-in user
|
||||
if err := a.storageManager.InitializeForUser(input.Email); err != nil {
|
||||
a.logger.Error("Failed to initialize user storage", zap.Error(err))
|
||||
// Don't fail login - user can still use cloud features, just not local storage
|
||||
} else {
|
||||
a.logger.Info("User storage initialized", zap.String("email", utils.MaskEmail(input.Email)))
|
||||
}
|
||||
|
||||
// Initialize search index for the logged-in user
|
||||
if err := a.searchService.Initialize(a.ctx, input.Email); err != nil {
|
||||
a.logger.Error("Failed to initialize search index", zap.Error(err))
|
||||
// Don't fail login if search initialization fails - it's not critical
|
||||
// The app can still function without search
|
||||
} else {
|
||||
a.logger.Info("Search index initialized", zap.String("email", utils.MaskEmail(input.Email)))
|
||||
|
||||
// Rebuild search index from local data in the background
|
||||
userEmail := input.Email // Capture email before goroutine
|
||||
go func() {
|
||||
if err := a.rebuildSearchIndexForUser(userEmail); err != nil {
|
||||
a.logger.Warn("Failed to rebuild search index after login", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Start token manager for automatic token refresh
|
||||
a.tokenManager.Start()
|
||||
a.logger.Info("Token manager started for new session")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecryptLoginChallenge decrypts the login challenge using the user's password.
|
||||
// The kdfAlgorithm parameter specifies which key derivation function to use.
|
||||
// If kdfAlgorithm is empty, it defaults to "PBKDF2-SHA256".
|
||||
func (a *Application) DecryptLoginChallenge(password, saltBase64, encryptedMasterKeyBase64, encryptedChallengeBase64, encryptedPrivateKeyBase64, publicKeyBase64, kdfAlgorithm string) (string, error) {
|
||||
// Default to PBKDF2-SHA256
|
||||
if kdfAlgorithm == "" {
|
||||
kdfAlgorithm = e2ee.PBKDF2Algorithm
|
||||
}
|
||||
|
||||
a.logger.Debug("Decrypting login challenge", zap.String("kdf_algorithm", kdfAlgorithm))
|
||||
|
||||
// Decode base64 inputs
|
||||
salt, err := base64.StdEncoding.DecodeString(saltBase64)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to decode salt", zap.Error(err))
|
||||
return "", fmt.Errorf("invalid salt encoding: %w", err)
|
||||
}
|
||||
|
||||
encryptedChallenge, err := base64.StdEncoding.DecodeString(encryptedChallengeBase64)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to decode encrypted challenge", zap.Error(err))
|
||||
return "", fmt.Errorf("invalid challenge encoding: %w", err)
|
||||
}
|
||||
|
||||
publicKey, err := base64.StdEncoding.DecodeString(publicKeyBase64)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to decode public key", zap.Error(err))
|
||||
return "", fmt.Errorf("invalid public key encoding: %w", err)
|
||||
}
|
||||
|
||||
// Decode encrypted private key
|
||||
encryptedPrivateKeyCombined, err := base64.StdEncoding.DecodeString(encryptedPrivateKeyBase64)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to decode encrypted private key", zap.Error(err))
|
||||
return "", fmt.Errorf("invalid encrypted private key encoding: %w", err)
|
||||
}
|
||||
|
||||
// Decode encrypted master key
|
||||
encryptedMasterKeyCombined, err := base64.StdEncoding.DecodeString(encryptedMasterKeyBase64)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to decode encrypted master key", zap.Error(err))
|
||||
return "", fmt.Errorf("invalid encrypted master key encoding: %w", err)
|
||||
}
|
||||
|
||||
// 1. Derive KEK from password and salt using PBKDF2-SHA256
|
||||
keychain, err := e2ee.NewSecureKeyChainWithAlgorithm(password, salt, kdfAlgorithm)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to create secure keychain", zap.Error(err), zap.String("kdf_algorithm", kdfAlgorithm))
|
||||
return "", fmt.Errorf("failed to derive key from password: %w", err)
|
||||
}
|
||||
defer keychain.Clear()
|
||||
|
||||
// 2. Decrypt master key with KEK into protected memory
|
||||
// Auto-detect nonce size: web frontend uses 24-byte nonces (XSalsa20), native uses 12-byte (ChaCha20)
|
||||
masterKeyNonce, masterKeyCiphertext, err := e2ee.SplitNonceAndCiphertextSecretBox(encryptedMasterKeyCombined)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to split encrypted master key", zap.Error(err))
|
||||
return "", fmt.Errorf("invalid encrypted master key format: %w", err)
|
||||
}
|
||||
|
||||
encryptedMasterKeyStruct := &e2ee.EncryptedKey{
|
||||
Ciphertext: masterKeyCiphertext,
|
||||
Nonce: masterKeyNonce,
|
||||
}
|
||||
|
||||
masterKey, err := keychain.DecryptMasterKeySecure(encryptedMasterKeyStruct)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to decrypt master key", zap.Error(err), zap.String("kdf_algorithm", kdfAlgorithm))
|
||||
return "", fmt.Errorf("failed to decrypt master key (wrong password?): %w", err)
|
||||
}
|
||||
defer masterKey.Destroy()
|
||||
|
||||
// 3. Decrypt private key with master key into protected memory
|
||||
// Auto-detect nonce size based on the encrypted data
|
||||
privateKeyNonce, privateKeyCiphertext, err := e2ee.SplitNonceAndCiphertextSecretBox(encryptedPrivateKeyCombined)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to split encrypted private key", zap.Error(err))
|
||||
return "", fmt.Errorf("invalid encrypted private key format: %w", err)
|
||||
}
|
||||
|
||||
encryptedPrivateKeyStruct := &e2ee.EncryptedKey{
|
||||
Ciphertext: privateKeyCiphertext,
|
||||
Nonce: privateKeyNonce,
|
||||
}
|
||||
|
||||
privateKey, err := e2ee.DecryptPrivateKeySecure(encryptedPrivateKeyStruct, masterKey)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to decrypt private key", zap.Error(err))
|
||||
return "", fmt.Errorf("failed to decrypt private key: %w", err)
|
||||
}
|
||||
defer privateKey.Destroy()
|
||||
|
||||
// 4. Decrypt the challenge using the private key (NaCl anonymous box)
|
||||
decryptedChallenge, err := e2ee.DecryptAnonymousBox(encryptedChallenge, publicKey, privateKey.Bytes())
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to decrypt challenge", zap.Error(err))
|
||||
return "", fmt.Errorf("failed to decrypt login challenge: %w", err)
|
||||
}
|
||||
|
||||
// Convert decrypted challenge to base64 for sending to server
|
||||
decryptedChallengeBase64 := base64.StdEncoding.EncodeToString(decryptedChallenge)
|
||||
|
||||
a.logger.Info("Successfully decrypted login challenge")
|
||||
return decryptedChallengeBase64, nil
|
||||
}
|
||||
|
||||
// RegistrationKeys contains all the E2EE keys needed for registration
|
||||
type RegistrationKeys struct {
|
||||
Salt string `json:"salt"`
|
||||
EncryptedMasterKey string `json:"encryptedMasterKey"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
EncryptedPrivateKey string `json:"encryptedPrivateKey"`
|
||||
EncryptedRecoveryKey string `json:"encryptedRecoveryKey"`
|
||||
MasterKeyEncryptedWithRecoveryKey string `json:"masterKeyEncryptedWithRecoveryKey"`
|
||||
// RecoveryMnemonic is the 12-word BIP39 mnemonic phrase that must be shown to the user
|
||||
// The user MUST save this phrase securely - it's their only way to recover their account
|
||||
RecoveryMnemonic string `json:"recoveryMnemonic"`
|
||||
}
|
||||
|
||||
// RecoveryInitiateResponse contains the response from initiating account recovery
|
||||
type RecoveryInitiateResponse struct {
|
||||
Message string `json:"message"`
|
||||
SessionID string `json:"sessionId"`
|
||||
EncryptedChallenge string `json:"encryptedChallenge"`
|
||||
}
|
||||
|
||||
// InitiateRecovery starts the account recovery process for the given email
|
||||
func (a *Application) InitiateRecovery(email string) (*RecoveryInitiateResponse, error) {
|
||||
// Validate input
|
||||
if err := inputvalidation.ValidateEmail(email); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check rate limit before making request
|
||||
if err := a.rateLimiter.Check(ratelimiter.OpRequestOTT, email); err != nil {
|
||||
a.logger.Warn("Recovery initiation rate limited",
|
||||
zap.String("email", utils.MaskEmail(email)),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := a.authService.InitiateRecovery(a.ctx, email, "recovery_key")
|
||||
if err != nil {
|
||||
a.logger.Error("Recovery initiation failed", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.logger.Info("Recovery initiated successfully", zap.String("email", utils.MaskEmail(email)))
|
||||
|
||||
return &RecoveryInitiateResponse{
|
||||
Message: resp.Message,
|
||||
SessionID: resp.SessionID,
|
||||
EncryptedChallenge: resp.EncryptedChallenge,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecryptRecoveryChallengeInput contains the data needed to process recovery challenge
|
||||
type DecryptRecoveryChallengeInput struct {
|
||||
RecoveryMnemonic string `json:"recoveryMnemonic"`
|
||||
EncryptedChallenge string `json:"encryptedChallenge"`
|
||||
}
|
||||
|
||||
// DecryptRecoveryChallengeResult contains the result of processing recovery challenge
|
||||
type DecryptRecoveryChallengeResult struct {
|
||||
DecryptedChallenge string `json:"decryptedChallenge"`
|
||||
IsValid bool `json:"isValid"`
|
||||
}
|
||||
|
||||
// DecryptRecoveryChallenge validates the recovery mnemonic and processes the challenge.
|
||||
// Note: The backend currently sends an unencrypted challenge (base64-encoded plaintext).
|
||||
// This function validates the recovery phrase format and passes through the challenge.
|
||||
// When the backend implements proper encryption, this function will decrypt the challenge.
|
||||
func (a *Application) DecryptRecoveryChallenge(input *DecryptRecoveryChallengeInput) (*DecryptRecoveryChallengeResult, error) {
|
||||
// Validate recovery mnemonic (must be 12 words)
|
||||
if input.RecoveryMnemonic == "" {
|
||||
return nil, fmt.Errorf("recovery mnemonic is required")
|
||||
}
|
||||
|
||||
// Validate the mnemonic is a valid BIP39 phrase
|
||||
if !bip39.IsMnemonicValid(input.RecoveryMnemonic) {
|
||||
a.logger.Warn("Invalid recovery mnemonic format")
|
||||
return nil, fmt.Errorf("invalid recovery phrase: must be 12 valid BIP39 words")
|
||||
}
|
||||
|
||||
// Count words to ensure we have exactly 12
|
||||
words := len(splitMnemonic(input.RecoveryMnemonic))
|
||||
if words != 12 {
|
||||
return nil, fmt.Errorf("invalid recovery phrase: must be exactly 12 words, got %d", words)
|
||||
}
|
||||
|
||||
// Validate the encrypted challenge is present
|
||||
if input.EncryptedChallenge == "" {
|
||||
return nil, fmt.Errorf("encrypted challenge is required")
|
||||
}
|
||||
|
||||
// Derive recovery key from mnemonic to validate it's a valid recovery phrase
|
||||
// This also prepares for future decryption when backend implements encryption
|
||||
seed := bip39.NewSeed(input.RecoveryMnemonic, "")
|
||||
recoveryKey := seed[:32]
|
||||
|
||||
a.logger.Debug("Recovery key derived successfully",
|
||||
zap.Int("key_length", len(recoveryKey)),
|
||||
zap.Int("word_count", words))
|
||||
|
||||
// TEMPORARY WORKAROUND: Backend currently sends base64-encoded plaintext challenge
|
||||
// instead of encrypted challenge. See backend TODO in recovery_initiate.go:108-113
|
||||
// Until backend implements proper encryption, we just validate and pass through.
|
||||
|
||||
// Decode the challenge to validate it's valid base64
|
||||
challengeBytes, err := base64.StdEncoding.DecodeString(input.EncryptedChallenge)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to decode challenge", zap.Error(err))
|
||||
return nil, fmt.Errorf("invalid challenge format: %w", err)
|
||||
}
|
||||
|
||||
// Re-encode to base64 for sending to backend
|
||||
decryptedChallengeBase64 := base64.StdEncoding.EncodeToString(challengeBytes)
|
||||
|
||||
a.logger.Info("Recovery challenge processed successfully (backend workaround active)")
|
||||
|
||||
return &DecryptRecoveryChallengeResult{
|
||||
DecryptedChallenge: decryptedChallengeBase64,
|
||||
IsValid: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// splitMnemonic splits a mnemonic phrase into words
|
||||
func splitMnemonic(mnemonic string) []string {
|
||||
var words []string
|
||||
for _, word := range splitByWhitespace(mnemonic) {
|
||||
if word != "" {
|
||||
words = append(words, word)
|
||||
}
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
// splitByWhitespace splits a string by whitespace characters
|
||||
func splitByWhitespace(s string) []string {
|
||||
return splitString(s)
|
||||
}
|
||||
|
||||
// splitString splits a string into words by spaces
|
||||
func splitString(s string) []string {
|
||||
var result []string
|
||||
word := ""
|
||||
for _, r := range s {
|
||||
if r == ' ' || r == '\t' || r == '\n' || r == '\r' {
|
||||
if word != "" {
|
||||
result = append(result, word)
|
||||
word = ""
|
||||
}
|
||||
} else {
|
||||
word += string(r)
|
||||
}
|
||||
}
|
||||
if word != "" {
|
||||
result = append(result, word)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// RecoveryVerifyResponse contains the response from verifying recovery
|
||||
type RecoveryVerifyResponse struct {
|
||||
Message string `json:"message"`
|
||||
RecoveryToken string `json:"recoveryToken"`
|
||||
CanResetCredentials bool `json:"canResetCredentials"`
|
||||
}
|
||||
|
||||
// VerifyRecovery verifies the recovery challenge with the server
|
||||
func (a *Application) VerifyRecovery(sessionID, decryptedChallenge string) (*RecoveryVerifyResponse, error) {
|
||||
if sessionID == "" {
|
||||
return nil, fmt.Errorf("session ID is required")
|
||||
}
|
||||
if decryptedChallenge == "" {
|
||||
return nil, fmt.Errorf("decrypted challenge is required")
|
||||
}
|
||||
|
||||
input := &client.RecoveryVerifyInput{
|
||||
SessionID: sessionID,
|
||||
DecryptedChallenge: decryptedChallenge,
|
||||
}
|
||||
|
||||
resp, err := a.authService.VerifyRecovery(a.ctx, input)
|
||||
if err != nil {
|
||||
a.logger.Error("Recovery verification failed", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.logger.Info("Recovery verification successful")
|
||||
|
||||
return &RecoveryVerifyResponse{
|
||||
Message: resp.Message,
|
||||
RecoveryToken: resp.RecoveryToken,
|
||||
CanResetCredentials: resp.CanResetCredentials,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CompleteRecoveryInput contains the data needed to complete account recovery
|
||||
type CompleteRecoveryInput struct {
|
||||
RecoveryToken string `json:"recoveryToken"`
|
||||
RecoveryMnemonic string `json:"recoveryMnemonic"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
|
||||
// CompleteRecoveryResponse contains the response from completing recovery
|
||||
type CompleteRecoveryResponse struct {
|
||||
Message string `json:"message"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// CompleteRecovery completes the account recovery by re-encrypting keys with a new password.
|
||||
// This function:
|
||||
// 1. Validates the recovery mnemonic
|
||||
// 2. Derives the recovery key from the mnemonic
|
||||
// 3. Generates new encryption keys with the new password
|
||||
// 4. Sends the new encrypted keys to the server
|
||||
func (a *Application) CompleteRecovery(input *CompleteRecoveryInput) (*CompleteRecoveryResponse, error) {
|
||||
// Validate inputs
|
||||
if input.RecoveryToken == "" {
|
||||
return nil, fmt.Errorf("recovery token is required")
|
||||
}
|
||||
if input.RecoveryMnemonic == "" {
|
||||
return nil, fmt.Errorf("recovery mnemonic is required")
|
||||
}
|
||||
if err := inputvalidation.ValidatePassword(input.NewPassword); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate the mnemonic is a valid BIP39 phrase
|
||||
if !bip39.IsMnemonicValid(input.RecoveryMnemonic) {
|
||||
return nil, fmt.Errorf("invalid recovery phrase: must be 12 valid BIP39 words")
|
||||
}
|
||||
|
||||
// Count words to ensure we have exactly 12
|
||||
words := len(splitMnemonic(input.RecoveryMnemonic))
|
||||
if words != 12 {
|
||||
return nil, fmt.Errorf("invalid recovery phrase: must be exactly 12 words, got %d", words)
|
||||
}
|
||||
|
||||
a.logger.Info("Starting recovery completion - generating new encryption keys")
|
||||
|
||||
// 1. Derive recovery key from mnemonic
|
||||
seed := bip39.NewSeed(input.RecoveryMnemonic, "")
|
||||
recoveryKeyBytes := seed[:32]
|
||||
recoveryKey, err := e2ee.NewSecureBuffer(recoveryKeyBytes)
|
||||
if err != nil {
|
||||
e2ee.ClearBytes(recoveryKeyBytes)
|
||||
return nil, fmt.Errorf("failed to create secure buffer for recovery key: %w", err)
|
||||
}
|
||||
defer recoveryKey.Destroy()
|
||||
e2ee.ClearBytes(recoveryKeyBytes)
|
||||
|
||||
// 2. Generate new salt for the new password
|
||||
newSalt, err := e2ee.GenerateSalt()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate new salt: %w", err)
|
||||
}
|
||||
|
||||
// 3. Create new keychain with PBKDF2-SHA256 (for web frontend compatibility)
|
||||
newKeychain, err := e2ee.NewSecureKeyChainWithAlgorithm(input.NewPassword, newSalt, e2ee.PBKDF2Algorithm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new keychain: %w", err)
|
||||
}
|
||||
defer newKeychain.Clear()
|
||||
|
||||
// 4. Generate new master key
|
||||
masterKeyBytes, err := e2ee.GenerateMasterKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate new master key: %w", err)
|
||||
}
|
||||
masterKey, err := e2ee.NewSecureBuffer(masterKeyBytes)
|
||||
if err != nil {
|
||||
e2ee.ClearBytes(masterKeyBytes)
|
||||
return nil, fmt.Errorf("failed to create secure buffer for master key: %w", err)
|
||||
}
|
||||
defer masterKey.Destroy()
|
||||
e2ee.ClearBytes(masterKeyBytes)
|
||||
|
||||
// 5. Encrypt master key with new KEK
|
||||
encryptedMasterKey, err := newKeychain.EncryptMasterKeySecretBox(masterKey.Bytes())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt master key: %w", err)
|
||||
}
|
||||
|
||||
// 6. Generate new keypair
|
||||
newPublicKey, privateKeyBytes, err := e2ee.GenerateKeyPair()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate new keypair: %w", err)
|
||||
}
|
||||
privateKey, err := e2ee.NewSecureBuffer(privateKeyBytes)
|
||||
if err != nil {
|
||||
e2ee.ClearBytes(privateKeyBytes)
|
||||
return nil, fmt.Errorf("failed to create secure buffer for private key: %w", err)
|
||||
}
|
||||
defer privateKey.Destroy()
|
||||
e2ee.ClearBytes(privateKeyBytes)
|
||||
|
||||
// 7. Encrypt private key with master key
|
||||
encryptedPrivateKey, err := e2ee.EncryptPrivateKeySecretBox(privateKey.Bytes(), masterKey.Bytes())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt private key: %w", err)
|
||||
}
|
||||
|
||||
// 8. Encrypt recovery key with master key
|
||||
encryptedRecoveryKey, err := e2ee.EncryptRecoveryKeySecretBox(recoveryKey.Bytes(), masterKey.Bytes())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt recovery key: %w", err)
|
||||
}
|
||||
|
||||
// 9. Encrypt master key with recovery key (for future recovery)
|
||||
masterKeyEncryptedWithRecoveryKey, err := e2ee.EncryptMasterKeyWithRecoveryKeySecretBox(masterKey.Bytes(), recoveryKey.Bytes())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt master key with recovery key: %w", err)
|
||||
}
|
||||
|
||||
// 10. Convert all keys to base64 for transport
|
||||
newSaltBase64 := base64.StdEncoding.EncodeToString(newSalt)
|
||||
newPublicKeyBase64 := base64.StdEncoding.EncodeToString(newPublicKey)
|
||||
newEncryptedMasterKeyBase64 := base64.StdEncoding.EncodeToString(
|
||||
e2ee.CombineNonceAndCiphertext(encryptedMasterKey.Nonce, encryptedMasterKey.Ciphertext),
|
||||
)
|
||||
newEncryptedPrivateKeyBase64 := base64.StdEncoding.EncodeToString(
|
||||
e2ee.CombineNonceAndCiphertext(encryptedPrivateKey.Nonce, encryptedPrivateKey.Ciphertext),
|
||||
)
|
||||
newEncryptedRecoveryKeyBase64 := base64.StdEncoding.EncodeToString(
|
||||
e2ee.CombineNonceAndCiphertext(encryptedRecoveryKey.Nonce, encryptedRecoveryKey.Ciphertext),
|
||||
)
|
||||
newMasterKeyEncryptedWithRecoveryKeyBase64 := base64.StdEncoding.EncodeToString(
|
||||
e2ee.CombineNonceAndCiphertext(masterKeyEncryptedWithRecoveryKey.Nonce, masterKeyEncryptedWithRecoveryKey.Ciphertext),
|
||||
)
|
||||
|
||||
// 11. Call API to complete recovery
|
||||
apiInput := &client.RecoveryCompleteInput{
|
||||
RecoveryToken: input.RecoveryToken,
|
||||
NewSalt: newSaltBase64,
|
||||
NewPublicKey: newPublicKeyBase64,
|
||||
NewEncryptedMasterKey: newEncryptedMasterKeyBase64,
|
||||
NewEncryptedPrivateKey: newEncryptedPrivateKeyBase64,
|
||||
NewEncryptedRecoveryKey: newEncryptedRecoveryKeyBase64,
|
||||
NewMasterKeyEncryptedWithRecoveryKey: newMasterKeyEncryptedWithRecoveryKeyBase64,
|
||||
}
|
||||
|
||||
resp, err := a.authService.CompleteRecovery(a.ctx, apiInput)
|
||||
if err != nil {
|
||||
a.logger.Error("Recovery completion failed", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.logger.Info("Recovery completed successfully - new encryption keys set")
|
||||
|
||||
return &CompleteRecoveryResponse{
|
||||
Message: resp.Message,
|
||||
Success: resp.Success,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateRegistrationKeys generates all E2EE keys needed for user registration.
|
||||
// This function uses PBKDF2-SHA256 for key derivation and XSalsa20-Poly1305 (SecretBox)
|
||||
// for symmetric encryption to ensure compatibility with the web frontend.
|
||||
func (a *Application) GenerateRegistrationKeys(password string) (*RegistrationKeys, error) {
|
||||
// 1. Generate salt (16 bytes for PBKDF2)
|
||||
salt, err := e2ee.GenerateSalt()
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to generate salt", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. Create secure keychain using PBKDF2-SHA256 (compatible with web frontend)
|
||||
// This derives KEK from password + salt using PBKDF2-SHA256 with 100,000 iterations
|
||||
keychain, err := e2ee.NewSecureKeyChainWithAlgorithm(password, salt, e2ee.PBKDF2Algorithm)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to create secure keychain", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
defer keychain.Clear() // Clear sensitive data when done
|
||||
|
||||
// 3. Generate master key in protected memory
|
||||
masterKeyBytes, err := e2ee.GenerateMasterKey()
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to generate master key", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
masterKey, err := e2ee.NewSecureBuffer(masterKeyBytes)
|
||||
if err != nil {
|
||||
e2ee.ClearBytes(masterKeyBytes)
|
||||
a.logger.Error("Failed to create secure buffer for master key", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
defer masterKey.Destroy()
|
||||
e2ee.ClearBytes(masterKeyBytes)
|
||||
|
||||
// 4. Encrypt master key with KEK using XSalsa20-Poly1305 (SecretBox)
|
||||
// This produces 24-byte nonces compatible with web frontend's libsodium
|
||||
encryptedMasterKey, err := keychain.EncryptMasterKeySecretBox(masterKey.Bytes())
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to encrypt master key", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. Generate NaCl keypair for asymmetric encryption
|
||||
publicKey, privateKeyBytes, err := e2ee.GenerateKeyPair()
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to generate keypair", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
privateKey, err := e2ee.NewSecureBuffer(privateKeyBytes)
|
||||
if err != nil {
|
||||
e2ee.ClearBytes(privateKeyBytes)
|
||||
a.logger.Error("Failed to create secure buffer for private key", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
defer privateKey.Destroy()
|
||||
e2ee.ClearBytes(privateKeyBytes)
|
||||
|
||||
// 6. Encrypt private key with master key using XSalsa20-Poly1305 (SecretBox)
|
||||
encryptedPrivateKey, err := e2ee.EncryptPrivateKeySecretBox(privateKey.Bytes(), masterKey.Bytes())
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to encrypt private key", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 7. Generate BIP39 mnemonic (12 words) for account recovery
|
||||
// This matches the web frontend's approach for cross-platform compatibility
|
||||
entropy := make([]byte, 16) // 128 bits = 12 words
|
||||
if _, err := rand.Read(entropy); err != nil {
|
||||
a.logger.Error("Failed to generate entropy for recovery mnemonic", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recoveryMnemonic, err := bip39.NewMnemonic(entropy)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to generate recovery mnemonic", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
a.logger.Info("Generated 12-word recovery mnemonic")
|
||||
|
||||
// Convert mnemonic to seed (64 bytes via HMAC-SHA512) then take first 32 bytes
|
||||
// This matches web frontend's mnemonicToRecoveryKey() function
|
||||
seed := bip39.NewSeed(recoveryMnemonic, "") // Empty passphrase like web frontend
|
||||
recoveryKeyBytes := seed[:32] // Use first 32 bytes as recovery key
|
||||
|
||||
recoveryKey, err := e2ee.NewSecureBuffer(recoveryKeyBytes)
|
||||
if err != nil {
|
||||
e2ee.ClearBytes(recoveryKeyBytes)
|
||||
a.logger.Error("Failed to create secure buffer for recovery key", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
defer recoveryKey.Destroy()
|
||||
e2ee.ClearBytes(recoveryKeyBytes)
|
||||
|
||||
// 8. Encrypt recovery key with master key using XSalsa20-Poly1305 (SecretBox)
|
||||
encryptedRecoveryKey, err := e2ee.EncryptRecoveryKeySecretBox(recoveryKey.Bytes(), masterKey.Bytes())
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to encrypt recovery key", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 9. Encrypt master key with recovery key using XSalsa20-Poly1305 (SecretBox)
|
||||
masterKeyEncryptedWithRecoveryKey, err := e2ee.EncryptMasterKeyWithRecoveryKeySecretBox(masterKey.Bytes(), recoveryKey.Bytes())
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to encrypt master key with recovery key", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert all keys to base64 for transport
|
||||
// Combine nonce and ciphertext for each encrypted key
|
||||
encryptedMasterKeyBase64 := base64.StdEncoding.EncodeToString(
|
||||
e2ee.CombineNonceAndCiphertext(encryptedMasterKey.Nonce, encryptedMasterKey.Ciphertext),
|
||||
)
|
||||
encryptedPrivateKeyBase64 := base64.StdEncoding.EncodeToString(
|
||||
e2ee.CombineNonceAndCiphertext(encryptedPrivateKey.Nonce, encryptedPrivateKey.Ciphertext),
|
||||
)
|
||||
encryptedRecoveryKeyBase64 := base64.StdEncoding.EncodeToString(
|
||||
e2ee.CombineNonceAndCiphertext(encryptedRecoveryKey.Nonce, encryptedRecoveryKey.Ciphertext),
|
||||
)
|
||||
masterKeyEncryptedWithRecoveryKeyBase64 := base64.StdEncoding.EncodeToString(
|
||||
e2ee.CombineNonceAndCiphertext(masterKeyEncryptedWithRecoveryKey.Nonce, masterKeyEncryptedWithRecoveryKey.Ciphertext),
|
||||
)
|
||||
|
||||
a.logger.Info("Successfully generated E2EE registration keys using PBKDF2-SHA256 + XSalsa20-Poly1305")
|
||||
|
||||
return &RegistrationKeys{
|
||||
Salt: base64.StdEncoding.EncodeToString(salt),
|
||||
EncryptedMasterKey: encryptedMasterKeyBase64,
|
||||
PublicKey: base64.StdEncoding.EncodeToString(publicKey),
|
||||
EncryptedPrivateKey: encryptedPrivateKeyBase64,
|
||||
EncryptedRecoveryKey: encryptedRecoveryKeyBase64,
|
||||
MasterKeyEncryptedWithRecoveryKey: masterKeyEncryptedWithRecoveryKeyBase64,
|
||||
RecoveryMnemonic: recoveryMnemonic,
|
||||
}, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue