package main // Tests for the customer-level credential model (round-2 item 2): // store-load convergence from legacy per-binding credentials, the // deterministic password rule, and the SS2022 user-key derivation. import ( "encoding/base64" "testing" ) func TestMigrateCustomerCredentialsConvergesFirstBinding(t *testing.T) { d := &Data{ Customers: []*Customer{{ID: "cus_1", Name: "Legacy"}}, NodeClients: []*NodeClient{ {ID: "ncl_1", NodeID: "nod_a", CustomerID: "cus_1", Credentials: &Credentials{ ClientID: "11111111-2222-3333-4444-000000000001", // A 32-byte root key: must be kept verbatim. RelayPassword: base64.StdEncoding.EncodeToString(make([]byte, 32)), ShortID: "abcd0001ef01", }}, {ID: "ncl_2", NodeID: "nod_b", CustomerID: "cus_1", Credentials: &Credentials{ ClientID: "22222222-2222-3333-4444-000000000002", RelayPassword: "other", ShortID: "ffff0002ef01", }}, }, } if !MigrateCustomerCredentials(d) { t.Fatalf("migration reported no change") } creds := d.Customers[0].Credentials if creds == nil || creds.ClientID != "11111111-2222-3333-4444-000000000001" || creds.ShortID != "abcd0001ef01" { t.Fatalf("converged set is not the first binding's: %+v", creds) } if creds.RelayPassword != base64.StdEncoding.EncodeToString(make([]byte, 32)) { t.Fatalf("32-byte root key must be kept, got %q", creds.RelayPassword) } for _, nc := range d.NodeClients { if nc.Credentials != nil { t.Fatalf("binding %s still carries credentials", nc.ID) } } if MigrateCustomerCredentials(d) { t.Fatalf("migration must be idempotent") } } func TestMigrateCustomerCredentialsRegeneratesShortPasswordOnly(t *testing.T) { d := &Data{ Customers: []*Customer{{ID: "cus_1"}}, NodeClients: []*NodeClient{ {ID: "ncl_1", NodeID: "nod_a", CustomerID: "cus_1", Credentials: &Credentials{ ClientID: "11111111-2222-3333-4444-000000000001", // 16-byte key (an old SS2022-aes128 binding set): too short // to derive a 32-byte user key from. RelayPassword: "FRYXGBkaGxwdHh8gISIjJA==", ShortID: "abcd0001ef01", }}, }, } MigrateCustomerCredentials(d) creds := d.Customers[0].Credentials if creds.ClientID != "11111111-2222-3333-4444-000000000001" || creds.ShortID != "abcd0001ef01" { t.Fatalf("clientId/shortId must be preserved: %+v", creds) } raw, err := base64.StdEncoding.DecodeString(creds.RelayPassword) if err != nil || len(raw) != 32 { t.Fatalf("password must be regenerated as a 32-byte root key, got %q", creds.RelayPassword) } if creds.RelayPassword == "FRYXGBkaGxwdHh8gISIjJA==" { t.Fatalf("short password must not survive convergence") } } func TestMigrateCustomerCredentialsFreshForBindingless(t *testing.T) { d := &Data{Customers: []*Customer{{ID: "cus_new"}}} if !MigrateCustomerCredentials(d) { t.Fatalf("expected a change") } creds := d.Customers[0].Credentials if creds == nil || creds.ClientID == "" || creds.ShortID == "" { t.Fatalf("fresh set incomplete: %+v", creds) } if raw, err := base64.StdEncoding.DecodeString(creds.RelayPassword); err != nil || len(raw) != 32 { t.Fatalf("fresh set password is not a 32-byte root key") } } func TestEffectiveRelayPassword(t *testing.T) { root := make([]byte, 32) for i := range root { root[i] = byte(i) } rootB64 := base64.StdEncoding.EncodeToString(root) // aes128 (16-byte method): deterministic prefix truncation. if got := EffectiveRelayPassword("shadowsocks-2022-aes128", rootB64); got != base64.StdEncoding.EncodeToString(root[:16]) { t.Fatalf("aes128 derivation wrong: %q", got) } // aes256: the root key itself. if got := EffectiveRelayPassword("shadowsocks-2022-aes256", rootB64); got != rootB64 { t.Fatalf("aes256 derivation wrong: %q", got) } // Legacy methods and other protocols: verbatim. if got := EffectiveRelayPassword("shadowsocks-aes256-gcm", rootB64); got != rootB64 { t.Fatalf("legacy SS must be verbatim") } if got := EffectiveRelayPassword("hysteria2", "plain-password"); got != "plain-password" { t.Fatalf("non-SS must be verbatim") } // An exact-length legacy key passes through unchanged (identity). k16 := "FRYXGBkaGxwdHh8gISIjJA==" if got := EffectiveRelayPassword("shadowsocks-2022-aes128", k16); got != k16 { t.Fatalf("16-byte key on aes128 must be identity, got %q", got) } } func TestNewCustomerCredentialsShape(t *testing.T) { c := NewCustomerCredentials() if c.ClientID == "" || c.ShortID == "" { t.Fatalf("incomplete set: %+v", c) } if raw, err := base64.StdEncoding.DecodeString(c.RelayPassword); err != nil || len(raw) != 32 { t.Fatalf("relay password must be a base64 32-byte root key") } }