368 lines
9.3 KiB
Go
368 lines
9.3 KiB
Go
package security
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base32"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/pquerna/otp/totp"
|
|
)
|
|
|
|
// MFAMethod représente une méthode MFA
|
|
type MFAMethod struct {
|
|
ID string `json:"id"`
|
|
UserID string `json:"user_id"`
|
|
Type string `json:"type"` // totp, sms, email, backup
|
|
Secret string `json:"secret,omitempty"`
|
|
Phone string `json:"phone,omitempty"`
|
|
Email string `json:"email,omitempty"`
|
|
IsActive bool `json:"is_active"`
|
|
IsVerified bool `json:"is_verified"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
VerifiedAt time.Time `json:"verified_at,omitempty"`
|
|
LastUsedAt time.Time `json:"last_used_at,omitempty"`
|
|
}
|
|
|
|
// MFASession représente une session MFA
|
|
type MFASession struct {
|
|
ID string `json:"id"`
|
|
UserID string `json:"user_id"`
|
|
MethodID string `json:"method_id"`
|
|
Token string `json:"token"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
Used bool `json:"used"`
|
|
}
|
|
|
|
// MFAManager gère l'authentification multi-facteurs
|
|
type MFAManager struct {
|
|
methods map[string]*MFAMethod
|
|
sessions map[string]*MFASession
|
|
}
|
|
|
|
// NewMFAManager crée un nouveau gestionnaire MFA
|
|
func NewMFAManager() *MFAManager {
|
|
return &MFAManager{
|
|
methods: make(map[string]*MFAMethod),
|
|
sessions: make(map[string]*MFASession),
|
|
}
|
|
}
|
|
|
|
// GenerateTOTPSecret génère un secret TOTP
|
|
func (mfa *MFAManager) GenerateTOTPSecret(userID, email string) (*MFAMethod, error) {
|
|
// Générer un secret aléatoire
|
|
secret := make([]byte, 20)
|
|
if _, err := rand.Read(secret); err != nil {
|
|
return nil, fmt.Errorf("failed to generate secret: %w", err)
|
|
}
|
|
|
|
// Encoder en base32
|
|
secretBase32 := base32.StdEncoding.EncodeToString(secret)
|
|
|
|
// Créer la méthode TOTP
|
|
method := &MFAMethod{
|
|
ID: fmt.Sprintf("totp_%s", userID),
|
|
UserID: userID,
|
|
Type: "totp",
|
|
Secret: secretBase32,
|
|
IsActive: false,
|
|
IsVerified: false,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
mfa.methods[method.ID] = method
|
|
return method, nil
|
|
}
|
|
|
|
// GenerateTOTPQRCode génère le QR code pour TOTP
|
|
func (mfa *MFAManager) GenerateTOTPQRCode(method *MFAMethod, issuer, accountName string) string {
|
|
// Format: otpauth://totp/issuer:account?secret=secret&issuer=issuer
|
|
url := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s",
|
|
issuer, accountName, method.Secret, issuer)
|
|
return url
|
|
}
|
|
|
|
// VerifyTOTP vérifie un code TOTP
|
|
func (mfa *MFAManager) VerifyTOTP(methodID, code string) (bool, error) {
|
|
method, exists := mfa.methods[methodID]
|
|
if !exists {
|
|
return false, fmt.Errorf("method not found")
|
|
}
|
|
|
|
if method.Type != "totp" {
|
|
return false, fmt.Errorf("method is not TOTP")
|
|
}
|
|
|
|
// Vérifier le code TOTP
|
|
valid := totp.Validate(code, method.Secret)
|
|
if valid {
|
|
method.LastUsedAt = time.Now()
|
|
if !method.IsVerified {
|
|
method.IsVerified = true
|
|
method.VerifiedAt = time.Now()
|
|
}
|
|
}
|
|
|
|
return valid, nil
|
|
}
|
|
|
|
// GenerateBackupCodes génère des codes de sauvegarde
|
|
func (mfa *MFAManager) GenerateBackupCodes(userID string, count int) ([]string, error) {
|
|
codes := make([]string, count)
|
|
|
|
for i := 0; i < count; i++ {
|
|
// Générer un code de 8 caractères
|
|
codeBytes := make([]byte, 4)
|
|
if _, err := rand.Read(codeBytes); err != nil {
|
|
return nil, fmt.Errorf("failed to generate backup code: %w", err)
|
|
}
|
|
|
|
// Encoder en base32 et prendre les 8 premiers caractères
|
|
code := base32.StdEncoding.EncodeToString(codeBytes)[:8]
|
|
codes[i] = code
|
|
}
|
|
|
|
// Créer la méthode de sauvegarde
|
|
method := &MFAMethod{
|
|
ID: fmt.Sprintf("backup_%s", userID),
|
|
UserID: userID,
|
|
Type: "backup",
|
|
Secret: "", // Les codes sont stockés séparément
|
|
IsActive: true,
|
|
IsVerified: true,
|
|
CreatedAt: time.Now(),
|
|
VerifiedAt: time.Now(),
|
|
}
|
|
|
|
mfa.methods[method.ID] = method
|
|
return codes, nil
|
|
}
|
|
|
|
// VerifyBackupCode vérifie un code de sauvegarde
|
|
func (mfa *MFAManager) VerifyBackupCode(userID, code string) (bool, error) {
|
|
methodID := fmt.Sprintf("backup_%s", userID)
|
|
method, exists := mfa.methods[methodID]
|
|
if !exists {
|
|
return false, fmt.Errorf("backup method not found")
|
|
}
|
|
|
|
// Dans un vrai système, les codes seraient stockés de manière sécurisée
|
|
// Ici on simule la vérification
|
|
valid := len(code) == 8 && method.IsActive
|
|
if valid {
|
|
method.LastUsedAt = time.Now()
|
|
}
|
|
|
|
return valid, nil
|
|
}
|
|
|
|
// GenerateSMSMFA génère une méthode MFA par SMS
|
|
func (mfa *MFAManager) GenerateSMSMFA(userID, phone string) (*MFAMethod, error) {
|
|
method := &MFAMethod{
|
|
ID: fmt.Sprintf("sms_%s", userID),
|
|
UserID: userID,
|
|
Type: "sms",
|
|
Phone: phone,
|
|
IsActive: false,
|
|
IsVerified: false,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
mfa.methods[method.ID] = method
|
|
return method, nil
|
|
}
|
|
|
|
// SendSMSCode envoie un code SMS
|
|
func (mfa *MFAManager) SendSMSCode(methodID string) (string, error) {
|
|
method, exists := mfa.methods[methodID]
|
|
if !exists {
|
|
return "", fmt.Errorf("method not found")
|
|
}
|
|
|
|
if method.Type != "sms" {
|
|
return "", fmt.Errorf("method is not SMS")
|
|
}
|
|
|
|
// Générer un code à 6 chiffres
|
|
code := fmt.Sprintf("%06d", time.Now().UnixNano()%1000000)
|
|
|
|
// Dans un vrai système, on enverrait le SMS via un service
|
|
// Ici on simule l'envoi
|
|
fmt.Printf("SMS code sent to %s: %s\n", method.Phone, code)
|
|
|
|
return code, nil
|
|
}
|
|
|
|
// VerifySMSCode vérifie un code SMS
|
|
func (mfa *MFAManager) VerifySMSCode(methodID, code string) (bool, error) {
|
|
method, exists := mfa.methods[methodID]
|
|
if !exists {
|
|
return false, fmt.Errorf("method not found")
|
|
}
|
|
|
|
if method.Type != "sms" {
|
|
return false, fmt.Errorf("method is not SMS")
|
|
}
|
|
|
|
// Dans un vrai système, on vérifierait le code stocké
|
|
// Ici on simule la vérification
|
|
valid := len(code) == 6
|
|
if valid {
|
|
method.IsVerified = true
|
|
method.VerifiedAt = time.Now()
|
|
method.LastUsedAt = time.Now()
|
|
}
|
|
|
|
return valid, nil
|
|
}
|
|
|
|
// GenerateEmailMFA génère une méthode MFA par email
|
|
func (mfa *MFAManager) GenerateEmailMFA(userID, email string) (*MFAMethod, error) {
|
|
method := &MFAMethod{
|
|
ID: fmt.Sprintf("email_%s", userID),
|
|
UserID: userID,
|
|
Type: "email",
|
|
Email: email,
|
|
IsActive: false,
|
|
IsVerified: false,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
mfa.methods[method.ID] = method
|
|
return method, nil
|
|
}
|
|
|
|
// SendEmailCode envoie un code par email
|
|
func (mfa *MFAManager) SendEmailCode(methodID string) (string, error) {
|
|
method, exists := mfa.methods[methodID]
|
|
if !exists {
|
|
return "", fmt.Errorf("method not found")
|
|
}
|
|
|
|
if method.Type != "email" {
|
|
return "", fmt.Errorf("method is not email")
|
|
}
|
|
|
|
// Générer un code à 6 chiffres
|
|
code := fmt.Sprintf("%06d", time.Now().UnixNano()%1000000)
|
|
|
|
// Dans un vrai système, on enverrait l'email via un service
|
|
// Ici on simule l'envoi
|
|
fmt.Printf("Email code sent to %s: %s\n", method.Email, code)
|
|
|
|
return code, nil
|
|
}
|
|
|
|
// VerifyEmailCode vérifie un code email
|
|
func (mfa *MFAManager) VerifyEmailCode(methodID, code string) (bool, error) {
|
|
method, exists := mfa.methods[methodID]
|
|
if !exists {
|
|
return false, fmt.Errorf("method not found")
|
|
}
|
|
|
|
if method.Type != "email" {
|
|
return false, fmt.Errorf("method is not email")
|
|
}
|
|
|
|
// Dans un vrai système, on vérifierait le code stocké
|
|
// Ici on simule la vérification
|
|
valid := len(code) == 6
|
|
if valid {
|
|
method.IsVerified = true
|
|
method.VerifiedAt = time.Now()
|
|
method.LastUsedAt = time.Now()
|
|
}
|
|
|
|
return valid, nil
|
|
}
|
|
|
|
// GetUserMFAMethods récupère toutes les méthodes MFA d'un utilisateur
|
|
func (mfa *MFAManager) GetUserMFAMethods(userID string) []*MFAMethod {
|
|
methods := make([]*MFAMethod, 0)
|
|
|
|
for _, method := range mfa.methods {
|
|
if method.UserID == userID {
|
|
methods = append(methods, method)
|
|
}
|
|
}
|
|
|
|
return methods
|
|
}
|
|
|
|
// ActivateMFAMethod active une méthode MFA
|
|
func (mfa *MFAManager) ActivateMFAMethod(methodID string) error {
|
|
method, exists := mfa.methods[methodID]
|
|
if !exists {
|
|
return fmt.Errorf("method not found")
|
|
}
|
|
|
|
if !method.IsVerified {
|
|
return fmt.Errorf("method must be verified before activation")
|
|
}
|
|
|
|
method.IsActive = true
|
|
return nil
|
|
}
|
|
|
|
// DeactivateMFAMethod désactive une méthode MFA
|
|
func (mfa *MFAManager) DeactivateMFAMethod(methodID string) error {
|
|
method, exists := mfa.methods[methodID]
|
|
if !exists {
|
|
return fmt.Errorf("method not found")
|
|
}
|
|
|
|
method.IsActive = false
|
|
return nil
|
|
}
|
|
|
|
// DeleteMFAMethod supprime une méthode MFA
|
|
func (mfa *MFAManager) DeleteMFAMethod(methodID string) error {
|
|
if _, exists := mfa.methods[methodID]; !exists {
|
|
return fmt.Errorf("method not found")
|
|
}
|
|
|
|
delete(mfa.methods, methodID)
|
|
return nil
|
|
}
|
|
|
|
// RequireMFA vérifie si un utilisateur doit utiliser MFA
|
|
func (mfa *MFAManager) RequireMFA(userID string) bool {
|
|
methods := mfa.GetUserMFAMethods(userID)
|
|
|
|
for _, method := range methods {
|
|
if method.IsActive && method.IsVerified {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// ValidateMFALogin valide une connexion MFA
|
|
func (mfa *MFAManager) ValidateMFALogin(userID, methodID, code string) (bool, error) {
|
|
method, exists := mfa.methods[methodID]
|
|
if !exists {
|
|
return false, fmt.Errorf("method not found")
|
|
}
|
|
|
|
if method.UserID != userID {
|
|
return false, fmt.Errorf("method does not belong to user")
|
|
}
|
|
|
|
if !method.IsActive || !method.IsVerified {
|
|
return false, fmt.Errorf("method is not active or verified")
|
|
}
|
|
|
|
switch method.Type {
|
|
case "totp":
|
|
return mfa.VerifyTOTP(methodID, code)
|
|
case "sms":
|
|
return mfa.VerifySMSCode(methodID, code)
|
|
case "email":
|
|
return mfa.VerifyEmailCode(methodID, code)
|
|
case "backup":
|
|
return mfa.VerifyBackupCode(userID, code)
|
|
default:
|
|
return false, fmt.Errorf("unsupported method type")
|
|
}
|
|
}
|