273 lines
7.1 KiB
Go
273 lines
7.1 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rwadurian/mpc-system/pkg/crypto"
|
|
"github.com/rwadurian/mpc-system/services/account/domain/entities"
|
|
"github.com/rwadurian/mpc-system/services/account/domain/repositories"
|
|
"github.com/rwadurian/mpc-system/services/account/domain/value_objects"
|
|
)
|
|
|
|
// AccountDomainService provides domain logic for accounts
|
|
type AccountDomainService struct {
|
|
accountRepo repositories.AccountRepository
|
|
shareRepo repositories.AccountShareRepository
|
|
recoveryRepo repositories.RecoverySessionRepository
|
|
}
|
|
|
|
// NewAccountDomainService creates a new AccountDomainService
|
|
func NewAccountDomainService(
|
|
accountRepo repositories.AccountRepository,
|
|
shareRepo repositories.AccountShareRepository,
|
|
recoveryRepo repositories.RecoverySessionRepository,
|
|
) *AccountDomainService {
|
|
return &AccountDomainService{
|
|
accountRepo: accountRepo,
|
|
shareRepo: shareRepo,
|
|
recoveryRepo: recoveryRepo,
|
|
}
|
|
}
|
|
|
|
// CreateAccountInput represents input for creating an account
|
|
type CreateAccountInput struct {
|
|
Username string
|
|
Email string
|
|
Phone *string
|
|
PublicKey []byte
|
|
KeygenSessionID uuid.UUID
|
|
ThresholdN int
|
|
ThresholdT int
|
|
Shares []ShareInfo
|
|
}
|
|
|
|
// ShareInfo represents information about a key share
|
|
type ShareInfo struct {
|
|
ShareType value_objects.ShareType
|
|
PartyID string
|
|
PartyIndex int
|
|
DeviceType *string
|
|
DeviceID *string
|
|
}
|
|
|
|
// CreateAccount creates a new account with shares
|
|
func (s *AccountDomainService) CreateAccount(ctx context.Context, input CreateAccountInput) (*entities.Account, error) {
|
|
// Check username uniqueness
|
|
exists, err := s.accountRepo.ExistsByUsername(ctx, input.Username)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if exists {
|
|
return nil, entities.ErrDuplicateUsername
|
|
}
|
|
|
|
// Check email uniqueness
|
|
exists, err = s.accountRepo.ExistsByEmail(ctx, input.Email)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if exists {
|
|
return nil, entities.ErrDuplicateEmail
|
|
}
|
|
|
|
// Create account
|
|
account := entities.NewAccount(
|
|
input.Username,
|
|
input.Email,
|
|
input.PublicKey,
|
|
input.KeygenSessionID,
|
|
input.ThresholdN,
|
|
input.ThresholdT,
|
|
)
|
|
|
|
if input.Phone != nil {
|
|
account.SetPhone(*input.Phone)
|
|
}
|
|
|
|
// Validate account
|
|
if err := account.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create account in repository
|
|
if err := s.accountRepo.Create(ctx, account); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create shares
|
|
for _, shareInfo := range input.Shares {
|
|
share := entities.NewAccountShare(
|
|
account.ID,
|
|
shareInfo.ShareType,
|
|
shareInfo.PartyID,
|
|
shareInfo.PartyIndex,
|
|
)
|
|
|
|
if shareInfo.DeviceType != nil && shareInfo.DeviceID != nil {
|
|
share.SetDeviceInfo(*shareInfo.DeviceType, *shareInfo.DeviceID)
|
|
}
|
|
|
|
if err := share.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.shareRepo.Create(ctx, share); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return account, nil
|
|
}
|
|
|
|
// VerifySignature verifies a signature against an account's public key
|
|
func (s *AccountDomainService) VerifySignature(ctx context.Context, accountID value_objects.AccountID, message, signature []byte) (bool, error) {
|
|
account, err := s.accountRepo.GetByID(ctx, accountID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Parse public key
|
|
pubKey, err := crypto.ParsePublicKey(account.PublicKey)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Verify signature
|
|
valid := crypto.VerifySignature(pubKey, message, signature)
|
|
return valid, nil
|
|
}
|
|
|
|
// InitiateRecovery initiates account recovery
|
|
func (s *AccountDomainService) InitiateRecovery(ctx context.Context, accountID value_objects.AccountID, recoveryType value_objects.RecoveryType, oldShareType *value_objects.ShareType) (*entities.RecoverySession, error) {
|
|
// Get account
|
|
account, err := s.accountRepo.GetByID(ctx, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check if recovery can be initiated
|
|
if err := account.StartRecovery(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Update account status
|
|
if err := s.accountRepo.Update(ctx, account); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create recovery session
|
|
recoverySession := entities.NewRecoverySession(accountID, recoveryType)
|
|
if oldShareType != nil {
|
|
recoverySession.SetOldShareType(*oldShareType)
|
|
}
|
|
|
|
if err := recoverySession.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.recoveryRepo.Create(ctx, recoverySession); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return recoverySession, nil
|
|
}
|
|
|
|
// CompleteRecovery completes the recovery process
|
|
func (s *AccountDomainService) CompleteRecovery(ctx context.Context, recoverySessionID string, newPublicKey []byte, newKeygenSessionID uuid.UUID, newShares []ShareInfo) error {
|
|
// Get recovery session
|
|
recovery, err := s.recoveryRepo.GetByID(ctx, recoverySessionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Start keygen if still in requested state (transitions to in_progress)
|
|
if recovery.Status == value_objects.RecoveryStatusRequested {
|
|
if err := recovery.StartKeygen(newKeygenSessionID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Complete recovery session
|
|
if err := recovery.Complete(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get account
|
|
account, err := s.accountRepo.GetByID(ctx, recovery.AccountID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Complete account recovery
|
|
account.CompleteRecovery(newPublicKey, newKeygenSessionID)
|
|
|
|
// Deactivate old shares
|
|
if err := s.shareRepo.DeactivateByAccountID(ctx, account.ID); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create new shares
|
|
for _, shareInfo := range newShares {
|
|
share := entities.NewAccountShare(
|
|
account.ID,
|
|
shareInfo.ShareType,
|
|
shareInfo.PartyID,
|
|
shareInfo.PartyIndex,
|
|
)
|
|
|
|
if shareInfo.DeviceType != nil && shareInfo.DeviceID != nil {
|
|
share.SetDeviceInfo(*shareInfo.DeviceType, *shareInfo.DeviceID)
|
|
}
|
|
|
|
if err := s.shareRepo.Create(ctx, share); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update account
|
|
if err := s.accountRepo.Update(ctx, account); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update recovery session
|
|
if err := s.recoveryRepo.Update(ctx, recovery); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetActiveShares returns active shares for an account
|
|
func (s *AccountDomainService) GetActiveShares(ctx context.Context, accountID value_objects.AccountID) ([]*entities.AccountShare, error) {
|
|
return s.shareRepo.GetActiveByAccountID(ctx, accountID)
|
|
}
|
|
|
|
// CanAccountSign checks if an account has enough active shares to sign
|
|
func (s *AccountDomainService) CanAccountSign(ctx context.Context, accountID value_objects.AccountID) (bool, error) {
|
|
account, err := s.accountRepo.GetByID(ctx, accountID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if !account.CanLogin() {
|
|
return false, nil
|
|
}
|
|
|
|
shares, err := s.shareRepo.GetActiveByAccountID(ctx, accountID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Count active shares
|
|
activeCount := 0
|
|
for _, share := range shares {
|
|
if share.IsActive {
|
|
activeCount++
|
|
}
|
|
}
|
|
|
|
// Check if we have enough shares for threshold
|
|
return activeCount >= account.ThresholdT, nil
|
|
}
|