package cookie import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "hash/crc32" "strings" "testing" ) const ( testCookieKey = "def000008bf3d70e7012b7493c382d561e193218d0c74ab162fb0ea8029ce20e926531b4bcf0aaec9381152e6c161f198e06918b2d1aad67cc7cf40819a51ee328c63830" ) func TestNativeCodecDecodeFixture(t *testing.T) { codec, err := NewCodec(Config{ CookieName: "PrestaShop-test", CookieKey: testCookieKey, CookieIV: "vfRFMV42", }) if err != nil { t.Fatalf("NewCodec() error = %v", err) } session, err := codec.Decode(encodeFixtureCookie(t, codec)) if err != nil { t.Fatalf("Decode() error = %v", err) } if session.Values["id_lang"] != "1" { t.Fatalf("id_lang = %q, want 1", session.Values["id_lang"]) } if session.Values["id_currency"] != "1" { t.Fatalf("id_currency = %q, want 1", session.Values["id_currency"]) } if session.Values["checksum"] == "" { t.Fatalf("checksum should not be empty") } if session.Values["detect_language"] != "1" { t.Fatalf("detect_language = %q, want 1", session.Values["detect_language"]) } if session.GuestID != nil { t.Fatalf("guest_id = %v, want nil", session.GuestID) } } func TestNativeCodecRoundTrip(t *testing.T) { codec, err := NewCodec(Config{ CookieName: "PrestaShop-test", CookieKey: testCookieKey, CookieIV: "vfRFMV42", }) if err != nil { t.Fatalf("NewCodec() error = %v", err) } decoded, err := codec.Decode(encodeFixtureCookie(t, codec)) if err != nil { t.Fatalf("Decode() error = %v", err) } encoded, err := codec.Encode(decoded) if err != nil { t.Fatalf("Encode() error = %v", err) } redecoded, err := codec.Decode(encoded) if err != nil { t.Fatalf("Decode(encoded) error = %v", err) } if redecoded.Plaintext != decoded.Plaintext { t.Fatalf("plaintext mismatch after roundtrip\n got: %s\nwant: %s", redecoded.Plaintext, decoded.Plaintext) } } func TestNativeCodecEncodeRecomputesPrestashopChecksum(t *testing.T) { codec, err := NewCodec(Config{ CookieName: "PrestaShop-test", CookieKey: testCookieKey, CookieIV: "vfRFMV42", }) if err != nil { t.Fatalf("NewCodec() error = %v", err) } decoded, err := codec.Decode(encodeFixtureCookie(t, codec)) if err != nil { t.Fatalf("Decode() error = %v", err) } decoded.Values["iso_code_country"] = "PL" decoded.Values["id_currency"] = "6" decoded.Values["checksum"] = "stale" decoded.Plaintext = "" encoded, err := codec.Encode(decoded) if err != nil { t.Fatalf("Encode() error = %v", err) } redecoded, err := codec.Decode(encoded) if err != nil { t.Fatalf("Decode(encoded) error = %v", err) } pairs := strings.Split(redecoded.Plaintext, fieldSeparator) if len(pairs) < 2 { t.Fatalf("plaintext too short: %q", redecoded.Plaintext) } body := strings.Join(pairs[:len(pairs)-1], fieldSeparator) + fieldSeparator wantChecksum := fmt.Sprintf("%d", crc32.ChecksumIEEE([]byte("vfRFMV42"+body))) if got := redecoded.Values["checksum"]; got != wantChecksum { t.Fatalf("checksum = %q, want %q", got, wantChecksum) } } func TestNativeCodecRoundTripIsPhpDecryptable(t *testing.T) { codec, err := NewCodec(Config{ CookieName: "PrestaShop-test", CookieKey: testCookieKey, CookieIV: "vfRFMV42", }) if err != nil { t.Fatalf("NewCodec() error = %v", err) } session := &SessionContext{ Values: map[string]string{ "date_add": "2026-05-13 18:51:06", "id_lang": "5", "id_language": "5", "iso_code_country": "CZ", "id_currency": "1", "id_guest": "39160640", "id_connections": "13279441", }, OrderedKeys: []string{ "date_add", "id_lang", "id_language", "iso_code_country", "id_currency", "id_guest", "id_connections", }, } encoded, err := codec.Encode(session) if err != nil { t.Fatalf("Encode() error = %v", err) } raw, err := hex.DecodeString(encoded) if err != nil { t.Fatalf("hex.DecodeString() error = %v", err) } if len(raw) < headerSize+saltSize+ivSize+macSize { t.Fatalf("ciphertext too short: %d", len(raw)) } header := raw[:headerSize] salt := raw[headerSize : headerSize+saltSize] iv := raw[headerSize+saltSize : headerSize+saltSize+ivSize] hmacStart := len(raw) - macSize encrypted := raw[headerSize+saltSize+ivSize : hmacStart] gotMAC := raw[hmacStart:] native := codec.(*nativeCodec) keys, err := native.deriveKeys(salt) if err != nil { t.Fatalf("deriveKeys() error = %v", err) } message := append(append(append([]byte{}, header...), salt...), iv...) message = append(message, encrypted...) h := hmac.New(sha256.New, keys.akey) h.Write(message) wantMAC := h.Sum(nil) if !hmac.Equal(gotMAC, wantMAC) { t.Fatalf("MAC mismatch") } redecoded, err := codec.Decode(encoded) if err != nil { t.Fatalf("Decode(encoded) error = %v", err) } if redecoded.Plaintext != "date_add|2026-05-13 18:51:06¤id_lang|5¤id_language|5¤iso_code_country|CZ¤id_currency|1¤id_guest|39160640¤id_connections|13279441¤checksum|181610492" { t.Fatalf("unexpected plaintext = %q", redecoded.Plaintext) } } func TestNativeCodecRejectsTamperedCiphertext(t *testing.T) { codec, err := NewCodec(Config{ CookieName: "PrestaShop-test", CookieKey: testCookieKey, CookieIV: "vfRFMV42", }) if err != nil { t.Fatalf("NewCodec() error = %v", err) } decoded, err := codec.Decode(encodeFixtureCookie(t, codec)) if err != nil { t.Fatalf("Decode() error = %v", err) } encoded, err := codec.Encode(decoded) if err != nil { t.Fatalf("Encode() error = %v", err) } raw, err := hex.DecodeString(encoded) if err != nil { t.Fatalf("hex.DecodeString() error = %v", err) } raw[len(raw)-1] ^= 0x01 tampered := hex.EncodeToString(raw) if _, err := codec.Decode(tampered); err == nil { t.Fatalf("Decode(tampered) error = nil, want integrity failure") } } func TestNativeCodecRejectsTamperedPlaintextChecksum(t *testing.T) { codec, err := NewCodec(Config{ CookieName: "PrestaShop-test", CookieKey: testCookieKey, CookieIV: "vfRFMV42", }) if err != nil { t.Fatalf("NewCodec() error = %v", err) } native := codec.(*nativeCodec) plaintext := "date_add|2026-05-13 18:51:06¤id_lang|5¤id_language|5¤iso_code_country|CZ¤id_currency|9¤id_guest|39160640¤id_connections|13279441¤checksum|181610492" encoded, err := native.encryptInternal(plaintext) if err != nil { t.Fatalf("encryptInternal() error = %v", err) } if _, err := codec.Decode(encoded); err == nil { t.Fatalf("Decode() error = nil, want checksum mismatch") } } func TestSerializeCookieValuesMatchesPrestashopChecksumFormula(t *testing.T) { values := map[string]string{ "date_add": "2026-05-13 18:51:06", "id_lang": "5", "id_language": "5", "iso_code_country": "CZ", "id_currency": "1", "id_guest": "39160640", "id_connections": "13279441", "checksum": "stale", } orderedKeys := []string{ "date_add", "id_lang", "id_language", "iso_code_country", "id_currency", "id_guest", "id_connections", "checksum", } got := serializeCookieValues(values, orderedKeys, "vfRFMV42") want := "date_add|2026-05-13 18:51:06¤id_lang|5¤id_language|5¤iso_code_country|CZ¤id_currency|1¤id_guest|39160640¤id_connections|13279441¤checksum|181610492" if got != want { t.Fatalf("serializeCookieValues() = %q, want %q", got, want) } } func encodeFixtureCookie(t *testing.T, codec Codec) string { t.Helper() session := &SessionContext{ Values: map[string]string{ "id_lang": "1", "id_cart": "", "id_language": "1", "detect_language": "1", "id_currency": "1", }, OrderedKeys: []string{ "id_lang", "id_cart", "id_language", "detect_language", "id_currency", }, } encoded, err := codec.Encode(session) if err != nil { t.Fatalf("Encode() error = %v", err) } return encoded }