commit 76b66227e4108c7cc1144af0e9910bc3b9aebf41 Author: Marek Goc Date: Thu Nov 13 10:45:26 2025 +0100 cookie_decrypt diff --git a/cookie/cookie.go b/cookie/cookie.go new file mode 100644 index 0000000..1db1de7 --- /dev/null +++ b/cookie/cookie.go @@ -0,0 +1,213 @@ +package cookie + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + "strings" +) + +type DecryptEncrypt struct { + _new_cookie_key_ string +} + +func New() *DecryptEncrypt { + return &DecryptEncrypt{ + _new_cookie_key_: "def000008bf3d70e7012b7493c382d561e193218d0c74ab162fb0ea8029ce20e926531b4bcf0aaec9381152e6c161f198e06918b2d1aad67cc7cf40819a51ee328c63830", + } +} + +const ( + CurrentVersion = "\xDE\xF5\x02\x00" + KeyCurrentVersion = "\xDE\xF0\x00\x00" + SaltSize = 32 + IVSize = 16 + MacSize = 32 + MinCiphertextSize = 84 + KeyByteSize = 32 + ChecksumSize = 32 + HeaderSize = 4 + AuthInfo = "DefusePHP|V2|KeyForAuthentication" + EncInfo = "DefusePHP|V2|KeyForEncryption" + PBKDF2Iterations = 100000 +) + +// === DECRYPT === +func (d *DecryptEncrypt) DecryptInternal(ciphertextHex string, kp *KeyOrPassword, rawBinary bool) ([]byte, error) { + var ct []byte + var err error + if !rawBinary { + ct, err = d.hexToBin(ciphertextHex) + if err != nil { + return nil, errors.New("invalid hex") + } + } else { + ct = []byte(ciphertextHex) + } + + if len(ct) < MinCiphertextSize { + return nil, errors.New("ciphertext too short") + } + + // Extract parts + header := ct[:HeaderSize] + if string(header) != CurrentVersion { + return nil, errors.New("bad version") + } + salt := ct[HeaderSize : HeaderSize+SaltSize] + iv := ct[HeaderSize+SaltSize : HeaderSize+SaltSize+IVSize] + hmacStart := len(ct) - MacSize + // expectedHMAC := ct[hmacStart:] + encStart := HeaderSize + SaltSize + IVSize + encrypted := ct[encStart:hmacStart] + + // Derive keys + keys, err := kp.DeriveKeys(salt) + if err != nil { + return nil, err + } + + // === HMAC: salt || iv || encrypted === + // message := append(append(salt, iv...), encrypted...) + // if !verifyHMAC(expectedHMAC, message, keys.akey) { + // return nil, errors.New("integrity check failed") + // } + + // Decrypt + return d.aesCTRDecrypt(encrypted, keys.ekey, iv) +} + +func (d *DecryptEncrypt) aesCTRDecrypt(ciphertext, key, iv []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + plaintext := make([]byte, len(ciphertext)) + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(plaintext, ciphertext) + return plaintext, nil +} + +// === ENCRYPT === +func (d *DecryptEncrypt) EncryptInternal(plaintext, domain string) (string, error) { + key, err := d.LoadKeyFromASCII() + if err != nil { + return "", err + } + kp := &KeyOrPassword{SecretType: 1, Key: key} + + salt := make([]byte, SaltSize) + if _, err := rand.Read(salt); err != nil { + return "", err + } + + iv := make([]byte, IVSize) + if _, err := rand.Read(iv); err != nil { + return "", err + } + + keys, err := kp.DeriveKeys(salt) + if err != nil { + return "", err + } + + encrypted, err := d.aesCTREncrypt([]byte(plaintext), keys.ekey, iv) + if err != nil { + return "", err + } + + // HMAC over: salt || iv || ciphertext + message := append(append(salt, iv...), encrypted...) + hmacVal := hmac.New(sha256.New, keys.akey) + hmacVal.Write(message) + mac := hmacVal.Sum(nil) + + // Assemble: header || salt || iv || encrypted || mac + result := append([]byte(CurrentVersion), salt...) + result = append(result, iv...) + result = append(result, encrypted...) + result = append(result, mac...) + + return hex.EncodeToString(result), nil +} + +func (d DecryptEncrypt) aesCTREncrypt(plaintext, key, iv []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + ciphertext := make([]byte, len(plaintext)) + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(ciphertext, plaintext) + return ciphertext, nil +} + +func (d *DecryptEncrypt) LoadKeyFromASCII() (*Key, error) { + b, err := d.loadBytesFromChecksummedASCII(KeyCurrentVersion) + if err != nil { + return nil, err + } + if len(b) != KeyByteSize { + return nil, errors.New("bad key length") + } + return &Key{bytes: b}, nil +} + +func (d *DecryptEncrypt) loadBytesFromChecksummedASCII(expectedHeader string) ([]byte, error) { + data, err := d.hexToBin(d._new_cookie_key_) + if err != nil { + return nil, err + } + if len(data) < HeaderSize+ChecksumSize { + return nil, errors.New("data too short") + } + if string(data[:HeaderSize]) != expectedHeader { + return nil, errors.New("invalid header") + } + payloadLen := len(data) - ChecksumSize + checked := data[:payloadLen] + sum := sha256.Sum256(checked) + if !hmac.Equal(sum[:], data[payloadLen:]) { + return nil, errors.New("checksum mismatch") + } + return data[HeaderSize:payloadLen], nil +} + +// func verifyHMAC(expected, msg, key []byte) bool { +// mac := hmac.New(sha256.New, key) +// mac.Write(msg) +// return hmac.Equal(mac.Sum(nil), expected) +// } + +// === hexToBin === +func (d *DecryptEncrypt) hexToBin(s string) ([]byte, error) { + s = strings.ToLower(s) + if len(s)%2 != 0 { + return nil, errors.New("odd length hex") + } + b := make([]byte, len(s)/2) + for i := 0; i < len(s); i += 2 { + hi := d.hexChar(s[i]) + lo := d.hexChar(s[i+1]) + if hi < 0 || lo < 0 { + return nil, errors.New("invalid hex char") + } + b[i/2] = byte(hi<<4 | lo) + } + return b, nil +} + +func (d *DecryptEncrypt) hexChar(c byte) int { + switch { + case '0' <= c && c <= '9': + return int(c - '0') + case 'a' <= c && c <= 'f': + return int(c-'a') + 10 + default: + return -1 + } +} diff --git a/cookie/keyorpassword.go b/cookie/keyorpassword.go new file mode 100644 index 0000000..1ed7016 --- /dev/null +++ b/cookie/keyorpassword.go @@ -0,0 +1,67 @@ +package cookie + +import ( + "crypto/hmac" + "crypto/sha256" + "errors" + "hash" +) + +type KeyOrPassword struct { + SecretType int + Key *Key + Password string + DerivedKeys *DerivedKeys + Akey []byte + Ekey []byte +} + +type DerivedKeys struct{ akey, ekey []byte } + +type Key struct{ bytes []byte } + +func (k *Key) GetRawBytes() []byte { return k.bytes } + +func (kp *KeyOrPassword) DeriveKeys(salt []byte) (*DerivedKeys, error) { + if len(salt) != SaltSize { + return nil, errors.New("bad salt") + } + if kp.SecretType == 1 { + akey := kp.hkdf(sha256.New, kp.Key.GetRawBytes(), KeyByteSize, AuthInfo, salt) + ekey := kp.hkdf(sha256.New, kp.Key.GetRawBytes(), KeyByteSize, EncInfo, salt) + return &DerivedKeys{akey: akey, ekey: ekey}, nil + } + return nil, errors.New("unsupported") +} + +// === HKDF === +func (kp *KeyOrPassword) hkdf(hashFunc func() hash.Hash, ikm []byte, length int, info string, salt []byte) []byte { + digestLen := hashFunc().Size() + if salt == nil { + salt = make([]byte, digestLen) + } + + // Extract + prkMac := hmac.New(hashFunc, salt) + prkMac.Write(ikm) + prk := prkMac.Sum(nil) + + // Expand + var okm []byte + prev := []byte{} + counter := byte(1) + + for len(okm) < length { + h := hmac.New(hashFunc, prk) + h.Write(prev) + h.Write([]byte(info)) + h.Write([]byte{counter}) + step := h.Sum(nil) + + okm = append(okm, step...) + prev = step + counter++ + } + + return okm[:length] +} diff --git a/cookie/tools.go b/cookie/tools.go new file mode 100644 index 0000000..a6941cc --- /dev/null +++ b/cookie/tools.go @@ -0,0 +1,3 @@ +package cookie + +// === Key Loading === diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7ec3cc9 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module presta + +go 1.25.1 + +require github.com/golang-jwt/jwt/v5 v5.3.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a5fba20 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= diff --git a/main.go b/main.go new file mode 100644 index 0000000..e762b5a --- /dev/null +++ b/main.go @@ -0,0 +1,144 @@ +package main + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "log" + "presta/cookie" + "strings" +) + +// === PrestaShop Constants === +const ( + _PS_VERSION_ = "1.7.6.3" + // _NEW_COOKIE_KEY_ = "def000008bf3d70e7012b7493c382d561e193218d0c74ab162fb0ea8029ce20e926531b4bcf0aaec9381152e6c161f198e06918b2d1aad67cc7cf40819a51ee328c63830" + __COOKIE__ = "def5020099dce5cd9ecf197adb5532a74e3db2ed9cba3d59b98f365353099b710bd562efa48b6bad1ad0a12b2ee54de0fbfcc6baa0545a8234141b03bfc1fbbbb9061af5011764b9c4dfd9c0ddcad767a453e0cc24d6b4a7c524e6c49aabd66ecc390e1a964b6e81a051b171051c829542facbb36cf64fcfebf069906dcc95476578be3fe59aaae466cf70bd9c877d301d908ec3aa4f55366567f460dfefac1684ce381293e8d4138382a42716d6aaecdcc7" + __EXPECTED_RESULT__ = "date_add|2025-11-02 12:26:58¤id_lang|2¤id_currency|1¤id_guest|23381445¤id_connections|2245874¤checksum|1154356442" +) + +// === Crypto Constants === + +// === CookTest === +type CookTest struct { + content map[string]string + name string + domain string + path string + standalone bool + expire int64 +} + +func main() { + // === DECRYPTION TEST === + ct, err := NewCookTest("", "", nil, nil, false, false) + if err != nil { + log.Fatal("Decrypt failed:", err) + } + + dct := cookie.New() + if err != nil { + log.Fatal("New DecryptEncrypt failed:", err) + } + + fmt.Println("
")
+	for k, v := range ct.content {
+		fmt.Printf("%s: %s\n", k, v)
+	}
+	fmt.Printf("Match expected: %v\n", ct.String() == __EXPECTED_RESULT__)
+	fmt.Println("
") + + // === ENCRYPTION TEST === + newData := "date_add|2025-11-03 10:00:00¤id_lang|1¤id_currency|2¤id_guest|999999¤id_connections|888888¤checksum|1234567890" + encryptedHex, err := dct.EncryptInternal(newData, ct.domain) + if err != nil { + log.Fatal("Encrypt failed:", err) + } + fmt.Printf("Encrypted cookie: %s\n", encryptedHex) + + // Optional: Try decrypting it back + // ct2, _ := NewCookTestFromCookie(encryptedHex, ct.domain) + // fmt.Println("Re-decrypted:", ct2.String()) +} + +func NewCookTest(name, path string, expire *int64, sharedURLs []string, standalone, secure bool) (*CookTest, error) { + ct := &CookTest{ + content: make(map[string]string), + standalone: standalone, + } + + dct := cookie.New() + + if expire == nil { + ct.expire = 1728000 + } else { + ct.expire = *expire + } + + path = strings.Trim(path, "/\\") + if !standalone && path != "" { + path = "/" + path + } + if path != "" && path[0] != '/' { + path = "/" + path + } + ct.path = path + + ct.domain = ct.getDomain(sharedURLs) + + versionPart := "" + if !standalone { + versionPart = _PS_VERSION_ + } + ct.name = "PrestaShop-" + md5String(versionPart+name+ct.domain) + + fmt.Printf("name: %v\n", ct.name) + + key, err := dct.LoadKeyFromASCII() + if err != nil { + return nil, err + } + + kp := &cookie.KeyOrPassword{SecretType: 1, Key: key} + + plaintext, err := dct.DecryptInternal(__COOKIE__, kp, false) + if err != nil { + return nil, fmt.Errorf("decrypt failed: %w", err) + } + + ct.parseContent(string(plaintext)) + return ct, nil +} + +func (ct *CookTest) parseContent(data string) { + pairs := strings.Split(data, "¤") + for _, p := range pairs { + if strings.Contains(p, "|") { + parts := strings.SplitN(p, "|", 2) + if len(parts) == 2 { + ct.content[parts[0]] = parts[1] + } + } + } +} + +func (ct *CookTest) String() string { + order := []string{"date_add", "id_lang", "id_currency", "id_guest", "id_connections", "checksum"} + var parts []string + for _, k := range order { + if v, ok := ct.content[k]; ok { + parts = append(parts, k+"|"+v) + } + } + return strings.Join(parts, "¤") +} + +func (ct *CookTest) getDomain(sharedURLs []string) string { + return "localhost" +} + +// === MD5 === +func md5String(s string) string { + sum := md5.Sum([]byte(s)) + return hex.EncodeToString(sum[:]) +}