package authentication import ( "crypto/subtle" "fmt" "strings" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) type Mode string const ( ModeDisabled Mode = "disabled" ModeObserve Mode = "observe" ModeEnforce Mode = "enforce" ) const ( StatusAccepted = "accepted" StatusRejected = "rejected" StatusUnknownAccount = "unknown_account" StatusMissingCredential = "missing_credential" StatusUnconfigured = "unconfigured" ) type Result struct { Applicable bool Allowed bool Mode Mode Status string Source string } type Authenticator interface { Authenticate(envelope.FrameEnvelope) Result } func ParseMode(value string, fallback Mode) (Mode, error) { value = strings.ToLower(strings.TrimSpace(value)) if value == "" { value = string(fallback) } switch Mode(value) { case ModeDisabled, ModeObserve, ModeEnforce: return Mode(value), nil default: return "", fmt.Errorf("unsupported authentication mode %q", value) } } type GB32960PlatformAuthenticator struct { mode Mode credentials map[string][]string } func NewGB32960PlatformAuthenticator(mode Mode, credentials map[string][]string) *GB32960PlatformAuthenticator { normalized := make(map[string][]string, len(credentials)) for username, passwords := range credentials { username = strings.TrimSpace(username) if username == "" { continue } for _, password := range passwords { if password == "" { continue } normalized[username] = append(normalized[username], password) } } return &GB32960PlatformAuthenticator{mode: mode, credentials: normalized} } func (a *GB32960PlatformAuthenticator) Authenticate(env envelope.FrameEnvelope) Result { if env.Protocol != envelope.ProtocolGB32960 || env.MessageID != "0x05" || a == nil || a.mode == ModeDisabled { return Result{} } login := nestedMap(env.Parsed, "platform_login") username := strings.TrimSpace(textValue(login, "username")) password := textValue(login, "password") status := StatusRejected switch { case len(a.credentials) == 0: status = StatusUnconfigured case username == "" || password == "": status = StatusMissingCredential default: expected, ok := a.credentials[username] if !ok || len(expected) == 0 { status = StatusUnknownAccount } else if anyConstantTimeEqual(expected, password) { status = StatusAccepted } } source := "none" if status == StatusAccepted { source = "configured" } return resultForMode(a.mode, status, source) } type JT808Authenticator struct { mode Mode authCode string deviceTokens JT808DeviceTokenProvider } type JT808DeviceTokenProvider interface { JT808AuthToken(phone string) (string, bool) } func NewJT808Authenticator(mode Mode, authCode string, deviceTokens JT808DeviceTokenProvider) *JT808Authenticator { return &JT808Authenticator{mode: mode, authCode: authCode, deviceTokens: deviceTokens} } func (a *JT808Authenticator) Authenticate(env envelope.FrameEnvelope) Result { if env.Protocol != envelope.ProtocolJT808 || env.MessageID != "0x0102" || a == nil || a.mode == ModeDisabled { return Result{} } token := textValue(nestedMap(env.Parsed, "authentication"), "token") status := StatusRejected source := "none" switch { case token == "": status = StatusMissingCredential default: if a.authCode != "" && constantTimeEqual(a.authCode, token) { status = StatusAccepted source = "configured" break } deviceToken, knownDevice := "", false if a.deviceTokens != nil { deviceToken, knownDevice = a.deviceTokens.JT808AuthToken(env.Phone) } if knownDevice && deviceToken != "" && constantTimeEqual(deviceToken, token) { status = StatusAccepted source = "device" } else if a.authCode == "" && !knownDevice { status = StatusUnconfigured } } return resultForMode(a.mode, status, source) } func Apply(env *envelope.FrameEnvelope, result Result) { if env == nil || !result.Applicable { return } env.AuthenticationMode = string(result.Mode) env.AuthenticationStatus = result.Status env.AuthenticationEnforced = result.Mode == ModeEnforce } // RedactParsedCredentials removes convenience copies of secrets before parsed // fields are flattened and published. The original protocol frame remains in // raw_hex for restricted forensic access. func RedactParsedCredentials(env *envelope.FrameEnvelope) { if env == nil || env.Protocol != envelope.ProtocolGB32960 { return } login := nestedMap(env.Parsed, "platform_login") if login == nil { return } if password := textValue(login, "password"); password != "" { login["password_present"] = true } delete(login, "password") } func resultForMode(mode Mode, status string, source ...string) Result { allowed := status == StatusAccepted || mode != ModeEnforce credentialSource := "none" if len(source) > 0 && strings.TrimSpace(source[0]) != "" { credentialSource = strings.TrimSpace(source[0]) } return Result{Applicable: true, Allowed: allowed, Mode: mode, Status: status, Source: credentialSource} } func constantTimeEqual(expected string, actual string) bool { if len(expected) != len(actual) { return false } return subtle.ConstantTimeCompare([]byte(expected), []byte(actual)) == 1 } func anyConstantTimeEqual(expected []string, actual string) bool { matched := 0 for _, candidate := range expected { if len(candidate) == len(actual) { matched |= subtle.ConstantTimeCompare([]byte(candidate), []byte(actual)) } } return matched == 1 } func nestedMap(parent map[string]any, key string) map[string]any { if parent == nil { return nil } value, _ := parent[key].(map[string]any) return value } func textValue(values map[string]any, key string) string { if values == nil { return "" } value, ok := values[key] if !ok || value == nil { return "" } return strings.TrimSpace(fmt.Sprint(value)) }