package password import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strings" "golang.org/x/crypto/argon2" ) type params struct { memory uint32 // en KiB iterations uint32 parallelism uint8 saltLength uint32 keyLength uint32 } var defaultParams = ¶ms{ memory: 64 * 1024, // 64 MB iterations: 3, parallelism: 2, saltLength: 16, keyLength: 32, } // generateSalt crée un salt aléatoire cryptographiquement sûr func generateSalt(length uint32) ([]byte, error) { salt := make([]byte, length) if _, err := rand.Read(salt); err != nil { return nil, err } return salt, nil } // HashPassword génère un hash Argon2id encodé (salt + hash + paramètres inclus) func HashPassword(password string) (string, error) { salt, err := generateSalt(defaultParams.saltLength) if err != nil { return "", err } hash := argon2.IDKey( []byte(password), salt, defaultParams.iterations, defaultParams.memory, defaultParams.parallelism, defaultParams.keyLength, ) // Format standard: $argon2id$v=19$m=65536,t=3,p=2$salt$hash b64Salt := base64.RawStdEncoding.EncodeToString(salt) b64Hash := base64.RawStdEncoding.EncodeToString(hash) encoded := fmt.Sprintf( "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, defaultParams.memory, defaultParams.iterations, defaultParams.parallelism, b64Salt, b64Hash, ) return encoded, nil } // VerifyPassword compare un mot de passe en clair avec un hash stocké func VerifyPassword(password, encodedHash string) (bool, error) { p, salt, hash, err := decodeHash(encodedHash) if err != nil { return false, err } otherHash := argon2.IDKey( []byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength, ) // Comparaison en temps constant pour éviter les timing attacks if subtle.ConstantTimeCompare(hash, otherHash) == 1 { return true, nil } return false, nil } // decodeHash parse la chaîne encodée pour en extraire params, salt et hash func decodeHash(encodedHash string) (*params, []byte, []byte, error) { parts := strings.Split(encodedHash, "$") if len(parts) != 6 { return nil, nil, nil, errors.New("format de hash invalide") } var version int _, err := fmt.Sscanf(parts[2], "v=%d", &version) if err != nil { return nil, nil, nil, err } if version != argon2.Version { return nil, nil, nil, errors.New("version argon2 incompatible") } p := ¶ms{} _, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &p.memory, &p.iterations, &p.parallelism) if err != nil { return nil, nil, nil, err } salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return nil, nil, nil, err } p.saltLength = uint32(len(salt)) hash, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil { return nil, nil, nil, err } p.keyLength = uint32(len(hash)) return p, salt, hash, nil }