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
|
|
@ -0,0 +1,251 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth/recovery_complete.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/awnumar/memguard"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/crypto"
|
||||
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
|
||||
uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/auditlog"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/storage/cache/cassandracache"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/transaction"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
type RecoveryCompleteRequestDTO struct {
|
||||
RecoveryToken string `json:"recovery_token"`
|
||||
NewSalt string `json:"new_salt"`
|
||||
NewPublicKey string `json:"new_public_key"`
|
||||
NewEncryptedMasterKey string `json:"new_encrypted_master_key"`
|
||||
NewEncryptedPrivateKey string `json:"new_encrypted_private_key"`
|
||||
NewEncryptedRecoveryKey string `json:"new_encrypted_recovery_key"`
|
||||
NewMasterKeyEncryptedWithRecoveryKey string `json:"new_master_key_encrypted_with_recovery_key"`
|
||||
}
|
||||
|
||||
type RecoveryCompleteResponseDTO struct {
|
||||
Message string `json:"message"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type RecoveryCompleteService interface {
|
||||
Execute(ctx context.Context, req *RecoveryCompleteRequestDTO) (*RecoveryCompleteResponseDTO, error)
|
||||
}
|
||||
|
||||
type recoveryCompleteServiceImpl struct {
|
||||
logger *zap.Logger
|
||||
auditLogger auditlog.AuditLogger
|
||||
userGetByEmailUC uc_user.UserGetByEmailUseCase
|
||||
userUpdateUC uc_user.UserUpdateUseCase
|
||||
cache cassandracache.CassandraCacher
|
||||
}
|
||||
|
||||
func NewRecoveryCompleteService(
|
||||
logger *zap.Logger,
|
||||
auditLogger auditlog.AuditLogger,
|
||||
userGetByEmailUC uc_user.UserGetByEmailUseCase,
|
||||
userUpdateUC uc_user.UserUpdateUseCase,
|
||||
cache cassandracache.CassandraCacher,
|
||||
) RecoveryCompleteService {
|
||||
return &recoveryCompleteServiceImpl{
|
||||
logger: logger.Named("RecoveryCompleteService"),
|
||||
auditLogger: auditLogger,
|
||||
userGetByEmailUC: userGetByEmailUC,
|
||||
userUpdateUC: userUpdateUC,
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *recoveryCompleteServiceImpl) Execute(ctx context.Context, req *RecoveryCompleteRequestDTO) (*RecoveryCompleteResponseDTO, error) {
|
||||
// Create SAGA for recovery completion workflow
|
||||
saga := transaction.NewSaga("recovery-complete", s.logger)
|
||||
|
||||
s.logger.Info("starting recovery completion")
|
||||
|
||||
// Step 1: Validate recovery token from cache
|
||||
tokenKey := fmt.Sprintf("recovery_token:%s", req.RecoveryToken)
|
||||
emailBytes, err := s.cache.Get(ctx, tokenKey)
|
||||
if err != nil || emailBytes == nil {
|
||||
s.logger.Warn("Recovery token not found or expired")
|
||||
return nil, fmt.Errorf("invalid or expired recovery token")
|
||||
}
|
||||
|
||||
email := string(emailBytes)
|
||||
|
||||
// Step 2: Get user by email and backup current credentials
|
||||
user, err := s.userGetByEmailUC.Execute(ctx, email)
|
||||
if err != nil || user == nil {
|
||||
s.logger.Error("User not found during recovery completion", zap.String("email", validation.MaskEmail(email)))
|
||||
return nil, fmt.Errorf("recovery completion failed")
|
||||
}
|
||||
|
||||
// Backup current credentials for compensation (deep copy)
|
||||
var oldSecurityData *dom_user.UserSecurityData
|
||||
if user.SecurityData != nil {
|
||||
// Create a deep copy of security data
|
||||
oldSecurityData = &dom_user.UserSecurityData{
|
||||
PasswordSalt: make([]byte, len(user.SecurityData.PasswordSalt)),
|
||||
PublicKey: user.SecurityData.PublicKey,
|
||||
EncryptedMasterKey: user.SecurityData.EncryptedMasterKey,
|
||||
EncryptedPrivateKey: user.SecurityData.EncryptedPrivateKey,
|
||||
EncryptedRecoveryKey: user.SecurityData.EncryptedRecoveryKey,
|
||||
MasterKeyEncryptedWithRecoveryKey: user.SecurityData.MasterKeyEncryptedWithRecoveryKey,
|
||||
}
|
||||
copy(oldSecurityData.PasswordSalt, user.SecurityData.PasswordSalt)
|
||||
}
|
||||
|
||||
// Decode new encryption keys from base64
|
||||
// SECURITY: All decoded key material is wiped from memory after use
|
||||
newSalt, err := base64.StdEncoding.DecodeString(req.NewSalt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid salt format")
|
||||
}
|
||||
defer memguard.WipeBytes(newSalt)
|
||||
|
||||
newPublicKey, err := base64.StdEncoding.DecodeString(req.NewPublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid public key format")
|
||||
}
|
||||
defer memguard.WipeBytes(newPublicKey)
|
||||
|
||||
newEncryptedMasterKey, err := base64.StdEncoding.DecodeString(req.NewEncryptedMasterKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid encrypted master key format")
|
||||
}
|
||||
defer memguard.WipeBytes(newEncryptedMasterKey)
|
||||
|
||||
newEncryptedPrivateKey, err := base64.StdEncoding.DecodeString(req.NewEncryptedPrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid encrypted private key format")
|
||||
}
|
||||
defer memguard.WipeBytes(newEncryptedPrivateKey)
|
||||
|
||||
newEncryptedRecoveryKey, err := base64.StdEncoding.DecodeString(req.NewEncryptedRecoveryKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid encrypted recovery key format")
|
||||
}
|
||||
defer memguard.WipeBytes(newEncryptedRecoveryKey)
|
||||
|
||||
newMasterKeyEncryptedWithRecovery, err := base64.StdEncoding.DecodeString(req.NewMasterKeyEncryptedWithRecoveryKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid master key encrypted with recovery format")
|
||||
}
|
||||
defer memguard.WipeBytes(newMasterKeyEncryptedWithRecovery)
|
||||
|
||||
// Update user's encryption keys
|
||||
if user.SecurityData == nil {
|
||||
user.SecurityData = &dom_user.UserSecurityData{}
|
||||
}
|
||||
|
||||
// Parse the encrypted keys into their proper structures
|
||||
// Format: nonce (24 bytes) + ciphertext (remaining bytes)
|
||||
|
||||
// Update password salt
|
||||
user.SecurityData.PasswordSalt = newSalt
|
||||
|
||||
// Update public key (critical for login challenge encryption)
|
||||
user.SecurityData.PublicKey = crypto.PublicKey{
|
||||
Key: newPublicKey,
|
||||
}
|
||||
|
||||
// Update encrypted master key
|
||||
if len(newEncryptedMasterKey) > 24 {
|
||||
user.SecurityData.EncryptedMasterKey = crypto.EncryptedMasterKey{
|
||||
Nonce: newEncryptedMasterKey[:24],
|
||||
Ciphertext: newEncryptedMasterKey[24:],
|
||||
KeyVersion: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Update encrypted private key
|
||||
if len(newEncryptedPrivateKey) > 24 {
|
||||
user.SecurityData.EncryptedPrivateKey = crypto.EncryptedPrivateKey{
|
||||
Nonce: newEncryptedPrivateKey[:24],
|
||||
Ciphertext: newEncryptedPrivateKey[24:],
|
||||
}
|
||||
}
|
||||
|
||||
// Update encrypted recovery key
|
||||
if len(newEncryptedRecoveryKey) > 24 {
|
||||
user.SecurityData.EncryptedRecoveryKey = crypto.EncryptedRecoveryKey{
|
||||
Nonce: newEncryptedRecoveryKey[:24],
|
||||
Ciphertext: newEncryptedRecoveryKey[24:],
|
||||
}
|
||||
}
|
||||
|
||||
// Update master key encrypted with recovery key
|
||||
if len(newMasterKeyEncryptedWithRecovery) > 24 {
|
||||
user.SecurityData.MasterKeyEncryptedWithRecoveryKey = crypto.MasterKeyEncryptedWithRecoveryKey{
|
||||
Nonce: newMasterKeyEncryptedWithRecovery[:24],
|
||||
Ciphertext: newMasterKeyEncryptedWithRecovery[24:],
|
||||
}
|
||||
}
|
||||
|
||||
// Update user's modified timestamp
|
||||
user.ModifiedAt = time.Now()
|
||||
|
||||
// Step 3: Save updated user with new credentials (compensate: restore old credentials)
|
||||
// CRITICAL: This must succeed before token deletion to prevent account takeover
|
||||
if err := s.userUpdateUC.Execute(ctx, user); err != nil {
|
||||
s.logger.Error("Failed to update user with new keys", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to complete recovery")
|
||||
}
|
||||
|
||||
// Register compensation: restore old credentials if token deletion fails
|
||||
userCaptured := user
|
||||
oldSecurityDataCaptured := oldSecurityData
|
||||
saga.AddCompensation(func(ctx context.Context) error {
|
||||
s.logger.Warn("compensating: restoring old credentials",
|
||||
zap.String("user_id", userCaptured.ID.String()))
|
||||
|
||||
// Restore old security data
|
||||
userCaptured.SecurityData = oldSecurityDataCaptured
|
||||
userCaptured.ModifiedAt = time.Now()
|
||||
|
||||
if err := s.userUpdateUC.Execute(ctx, userCaptured); err != nil {
|
||||
s.logger.Error("Failed to restore old credentials during compensation",
|
||||
zap.String("user_id", userCaptured.ID.String()),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("compensation failed: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("old credentials restored successfully during compensation",
|
||||
zap.String("user_id", userCaptured.ID.String()))
|
||||
return nil
|
||||
})
|
||||
|
||||
// Step 4: Clear recovery token (one-time use) - MUST succeed to prevent reuse
|
||||
// CRITICAL: If this fails, recovery token could be reused for account takeover
|
||||
tokenKeyCaptured := tokenKey
|
||||
if err := s.cache.Delete(ctx, tokenKeyCaptured); err != nil {
|
||||
s.logger.Error("Failed to delete recovery token - SECURITY RISK",
|
||||
zap.String("token_key", tokenKeyCaptured),
|
||||
zap.Error(err))
|
||||
|
||||
// Trigger compensation: Restore old credentials
|
||||
saga.Rollback(ctx)
|
||||
|
||||
return nil, fmt.Errorf("failed to invalidate recovery token - please contact support")
|
||||
}
|
||||
|
||||
s.logger.Info("Recovery completion successful",
|
||||
zap.String("email", validation.MaskEmail(email)),
|
||||
zap.String("user_id", user.ID.String()))
|
||||
|
||||
// Audit log recovery completion
|
||||
s.auditLogger.LogAuth(ctx, auditlog.EventTypeRecoveryCompleted, auditlog.OutcomeSuccess,
|
||||
validation.MaskEmail(email), "", map[string]string{
|
||||
"user_id": user.ID.String(),
|
||||
})
|
||||
|
||||
return &RecoveryCompleteResponseDTO{
|
||||
Message: "Account recovery completed successfully. You can now log in with your new credentials.",
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue