Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/server/internal/security/totp.go
Fimeg a4d585c79c code review: 12-finding fan-out — fixes across server, agent, web
HIGH:
- URL sync race: page useEffect now preserves filter params from useFilterUrl
- useFilterUrl: document two-effect pattern (state→URL and URL→state)
- Fleet-join: store nil (not &"") for absent MachineID/PublicKeyFingerprint
- agents.go: same NULL fix for standard registration path

MEDIUM:
- Test assertions: replace CSS class checks with user-visible element assertions
- Updates vuln toggle: fixed-set like other quick filters (was toggling)
- ConfigureSecrets route: restore to welcome-mode server
- Config upgrade: recursive mergeMissingKeys for nested sub-fields + test

LOW:
- LiveOperations: wire FilterBar pills/clearAll/activeCount
- HashTOTPSeed: remove dead code replaced by encrypted storage (migration 058)
- auditor.go: replace unsafe reflect with Recorder wrapper (AUDIT-002)

History filter panel kept as-is (collapsible pattern intentional).
Agents.tsx duplicate buildFilterPills was a false positive (already resolved).
2026-06-11 17:38:08 -04:00

96 lines
2.9 KiB
Go

// Package security provides TOTP (RFC 6238) validation for fleet-join 2FA
// (SEC-025). The seed is stored AES-256-GCM encrypted on the registration
// token; the host generates the seed and the code. The server only validates.
package security
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"encoding/base32"
"encoding/binary"
"fmt"
"strings"
"time"
)
const (
// TotpPeriod is the time step in seconds (RFC 6238 default).
TotpPeriod = 30
// totpDigits is the number of digits in the TOTP code.
totpDigits = 6
// totpSkew is the number of time steps to accept in each direction.
// ±1 step = ±30 seconds, matching standard authenticator apps.
totpSkew = 1
)
// GenerateTOTPSeed creates a new random TOTP seed (base32-encoded, 20 bytes).
// Called by the standalone host; the server never generates seeds.
func GenerateTOTPSeed() (string, error) {
buf := make([]byte, 20) // 160 bits, standard for HMAC-SHA1
if _, err := rand.Read(buf); err != nil {
return "", fmt.Errorf("totp: seed generation failed: %w", err)
}
return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(buf), nil
}
// ValidTOTPSeed reports whether seed is valid base32 (the encoding TOTP uses).
// Used at token-creation time to reject garbage seeds before they're encrypted
// and stored — a seed that can't be decoded can never produce a valid code.
func ValidTOTPSeed(seed string) bool {
_, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(strings.ToUpper(seed))
return err == nil && len(seed) > 0
}
// ValidateTOTPCode checks whether the given 6-digit code is valid for the
// seed at the current time, with ±1 step tolerance.
func ValidateTOTPCode(seed string, code string) bool {
return ValidateTOTPCodeAt(seed, code, time.Now())
}
// ValidateTOTPCodeAt checks the code at a specific time (for testing).
func ValidateTOTPCodeAt(seed string, code string, t time.Time) bool {
if len(code) != totpDigits {
return false
}
seedBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(strings.ToUpper(seed))
if err != nil {
return false
}
counter := uint64(t.Unix()) / TotpPeriod
// Check current time step and ±skew.
for i := -totpSkew; i <= totpSkew; i++ {
c := counter + uint64(i)
if i < 0 && c > counter { // underflow guard
continue
}
if totpAt(seedBytes, c) == code {
return true
}
}
return false
}
// totpAt generates the TOTP code for a given counter value.
func totpAt(key []byte, counter uint64) string {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, counter)
mac := hmac.New(sha1.New, key)
mac.Write(buf)
sum := mac.Sum(nil)
// Dynamic truncation (RFC 4226 §5.4).
offset := sum[len(sum)-1] & 0x0f
code := binary.BigEndian.Uint32(sum[offset:offset+4]) & 0x7fffffff
// Format as zero-padded digit string.
divisor := uint32(1)
for i := 0; i < totpDigits; i++ {
divisor *= 10
}
return fmt.Sprintf("%0*d", totpDigits, code%divisor)
}