package lagoon import ( "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "strings" ) // laravelPayload is Laravel's Encrypter JSON body (iv/value/mac). type laravelPayload struct { IV string `json:"iv"` Value string `json:"value"` MAC string `json:"mac"` } // DecryptLaravelPayload decrypts a Laravel AES-256-CBC+HMAC "encrypted" payload // (base64 JSON {iv,value,mac}) using the raw APP_KEY bytes. // // Cutover-import-only; never called from Encrypted's live Scan/Value path (D-10). func DecryptLaravelPayload(payloadJSON string, appKey []byte) ([]byte, error) { if len(appKey) != encryptedKeySize { return nil, fmt.Errorf("lagoon: laravel payload key must be 32 bytes") } body, err := decodeLaravelJSON(payloadJSON) if err != nil { return nil, err } var payload laravelPayload if err := json.Unmarshal(body, &payload); err != nil { return nil, fmt.Errorf("lagoon: laravel payload json: %w", err) } if payload.IV == "" || payload.Value == "" || payload.MAC == "" { return nil, fmt.Errorf("lagoon: laravel payload missing iv, value, or mac") } wantMAC, err := hex.DecodeString(payload.MAC) if err != nil { return nil, fmt.Errorf("lagoon: laravel payload mac: %w", err) } mac := hmac.New(sha256.New, appKey) _, _ = mac.Write([]byte(payload.IV + payload.Value)) gotMAC := mac.Sum(nil) if !hmac.Equal(wantMAC, gotMAC) { return nil, fmt.Errorf("lagoon: laravel payload mac mismatch") } iv, err := base64.StdEncoding.DecodeString(payload.IV) if err != nil { return nil, fmt.Errorf("lagoon: laravel payload iv: %w", err) } ct, err := base64.StdEncoding.DecodeString(payload.Value) if err != nil { return nil, fmt.Errorf("lagoon: laravel payload value: %w", err) } if len(iv) != aes.BlockSize { return nil, fmt.Errorf("lagoon: laravel payload iv length %d", len(iv)) } if len(ct) == 0 || len(ct)%aes.BlockSize != 0 { return nil, fmt.Errorf("lagoon: laravel payload ciphertext length %d", len(ct)) } block, err := aes.NewCipher(appKey) if err != nil { return nil, err } plain := make([]byte, len(ct)) cipher.NewCBCDecrypter(block, iv).CryptBlocks(plain, ct) return pkcs7Unpad(plain, aes.BlockSize) } func decodeLaravelJSON(payloadJSON string) ([]byte, error) { s := strings.TrimSpace(payloadJSON) if s == "" { return nil, fmt.Errorf("lagoon: laravel payload is empty") } if decoded, err := base64.StdEncoding.DecodeString(s); err == nil { trimmed := strings.TrimSpace(string(decoded)) if strings.HasPrefix(trimmed, "{") { return []byte(trimmed), nil } } if strings.HasPrefix(s, "{") { return []byte(s), nil } return nil, fmt.Errorf("lagoon: laravel payload is not base64 JSON") } func pkcs7Unpad(b []byte, blockSize int) ([]byte, error) { if blockSize <= 0 || len(b) == 0 || len(b)%blockSize != 0 { return nil, fmt.Errorf("lagoon: laravel payload padding") } pad := int(b[len(b)-1]) if pad == 0 || pad > blockSize || pad > len(b) { return nil, fmt.Errorf("lagoon: laravel payload padding") } for i := len(b) - pad; i < len(b); i++ { if int(b[i]) != pad { return nil, fmt.Errorf("lagoon: laravel payload padding") } } return b[:len(b)-pad], nil }