feat: build vehicle data platform and production pipeline
This commit is contained in:
215
go/vehicle-gateway/internal/authentication/authentication.go
Normal file
215
go/vehicle-gateway/internal/authentication/authentication.go
Normal file
@@ -0,0 +1,215 @@
|
||||
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))
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
|
||||
)
|
||||
|
||||
func TestGB32960PlatformAuthenticatorModes(t *testing.T) {
|
||||
env := envelope.FrameEnvelope{
|
||||
Protocol: envelope.ProtocolGB32960,
|
||||
MessageID: "0x05",
|
||||
Parsed: map[string]any{
|
||||
"platform_login": map[string]any{"username": "platform-a", "password": "secret-a"},
|
||||
},
|
||||
}
|
||||
|
||||
accepted := NewGB32960PlatformAuthenticator(ModeEnforce, map[string][]string{"platform-a": {"secret-a"}}).Authenticate(env)
|
||||
if !accepted.Applicable || !accepted.Allowed || accepted.Status != StatusAccepted {
|
||||
t.Fatalf("accepted result = %#v", accepted)
|
||||
}
|
||||
|
||||
rejected := NewGB32960PlatformAuthenticator(ModeEnforce, map[string][]string{"platform-a": {"other"}}).Authenticate(env)
|
||||
if !rejected.Applicable || rejected.Allowed || rejected.Status != StatusRejected {
|
||||
t.Fatalf("enforced rejected result = %#v", rejected)
|
||||
}
|
||||
|
||||
observed := NewGB32960PlatformAuthenticator(ModeObserve, map[string][]string{"platform-a": {"other"}}).Authenticate(env)
|
||||
if !observed.Allowed || observed.Status != StatusRejected {
|
||||
t.Fatalf("observed rejected result = %#v", observed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGB32960PlatformAuthenticatorReportsUnknownAccount(t *testing.T) {
|
||||
env := envelope.FrameEnvelope{
|
||||
Protocol: envelope.ProtocolGB32960,
|
||||
MessageID: "0x05",
|
||||
Parsed: map[string]any{
|
||||
"platform_login": map[string]any{"username": "unknown", "password": "secret"},
|
||||
},
|
||||
}
|
||||
result := NewGB32960PlatformAuthenticator(ModeEnforce, map[string][]string{"known": {"secret"}}).Authenticate(env)
|
||||
if result.Allowed || result.Status != StatusUnknownAccount {
|
||||
t.Fatalf("result = %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJT808AuthenticatorValidatesAuthenticationFrame(t *testing.T) {
|
||||
env := envelope.FrameEnvelope{
|
||||
Protocol: envelope.ProtocolJT808,
|
||||
MessageID: "0x0102",
|
||||
Parsed: map[string]any{
|
||||
"authentication": map[string]any{"token": "issued-code"},
|
||||
},
|
||||
}
|
||||
accepted := NewJT808Authenticator(ModeEnforce, "issued-code", nil).Authenticate(env)
|
||||
if !accepted.Allowed || accepted.Status != StatusAccepted {
|
||||
t.Fatalf("accepted result = %#v", accepted)
|
||||
}
|
||||
rejected := NewJT808Authenticator(ModeEnforce, "different-code", nil).Authenticate(env)
|
||||
if rejected.Allowed || rejected.Status != StatusRejected {
|
||||
t.Fatalf("rejected result = %#v", rejected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJT808AuthenticatorAcceptsDeviceSnapshotToken(t *testing.T) {
|
||||
env := envelope.FrameEnvelope{
|
||||
Protocol: envelope.ProtocolJT808,
|
||||
MessageID: "0x0102",
|
||||
Phone: "013307795425",
|
||||
Parsed: map[string]any{
|
||||
"authentication": map[string]any{"token": "device-code"},
|
||||
},
|
||||
}
|
||||
provider := staticJT808DeviceTokens{"13307795425": "device-code"}
|
||||
result := NewJT808Authenticator(ModeEnforce, "configured-code", provider).Authenticate(env)
|
||||
if !result.Allowed || result.Status != StatusAccepted || result.Source != "device" {
|
||||
t.Fatalf("device token result = %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
type staticJT808DeviceTokens map[string]string
|
||||
|
||||
func (p staticJT808DeviceTokens) JT808AuthToken(phone string) (string, bool) {
|
||||
phone = strings.TrimLeft(phone, "0")
|
||||
token, ok := p[phone]
|
||||
return token, ok
|
||||
}
|
||||
|
||||
func TestRedactParsedCredentialsRemovesGB32960Password(t *testing.T) {
|
||||
login := map[string]any{"username": "platform-a", "password": "secret-a"}
|
||||
env := envelope.FrameEnvelope{
|
||||
Protocol: envelope.ProtocolGB32960,
|
||||
Parsed: map[string]any{"platform_login": login},
|
||||
}
|
||||
|
||||
RedactParsedCredentials(&env)
|
||||
if _, exists := login["password"]; exists {
|
||||
t.Fatalf("password remained in parsed login: %#v", login)
|
||||
}
|
||||
if login["password_present"] != true {
|
||||
t.Fatalf("password presence marker missing: %#v", login)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseModeRejectsUnknownValue(t *testing.T) {
|
||||
if _, err := ParseMode("strict-ish", ModeObserve); err == nil {
|
||||
t.Fatal("expected unsupported mode error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user