216 lines
5.7 KiB
Go
216 lines
5.7 KiB
Go
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))
|
|
}
|