Files
antrean-anjungan/internal/services/bpjs/response.go
2025-09-10 21:31:31 +07:00

1072 lines
32 KiB
Go

package services
import (
helper "api-service/internal/helpers/bpjs"
"bytes"
"compress/gzip"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"strings"
"unicode"
"unicode/utf16"
"unicode/utf8"
lzstring "github.com/daku10/go-lz-string"
)
func min(a, b int) int {
if a < b {
return a
}
return b
}
// GenerateBPJSKey - Generate key sesuai standar BPJS VClaim
func GenerateBPJSKey(consumerID, timestamp, userKey string) string {
// Format: consumerID + "&" + timestamp + "&" + userKey
keyString := fmt.Sprintf("%s&%s&%s", consumerID, timestamp, userKey)
// BPJS biasanya menggunakan key dengan length 32 bytes
if len(keyString) > 32 {
return keyString[:32]
}
// Pad dengan spasi jika kurang dari 32
for len(keyString) < 32 {
keyString += " "
}
log.Printf("GenerateBPJSKey: Generated key length: %d", len(keyString))
return keyString
}
// ResponseVclaim decrypts the encrypted response from VClaim API
func ResponseVclaim(encrypted string, key string) (string, error) {
log.Println("ResponseVclaim: Starting decryption process")
log.Printf("ResponseVclaim: Encrypted string length: %d", len(encrypted))
// Pad the base64 string if needed
if len(encrypted)%4 != 0 {
padding := (4 - len(encrypted)%4) % 4
for i := 0; i < padding; i++ {
encrypted += "="
}
}
// Decode base64
cipherText, err := base64.StdEncoding.DecodeString(encrypted)
if err != nil {
log.Printf("ResponseVclaim: Failed to decode base64: %v", err)
return "", err
}
if len(cipherText) < aes.BlockSize {
return "", errors.New("cipherText too short")
}
// Try multiple key generation and decryption methods
keyMethods := []func(string) ([]byte, error){
// Method 1: SHA256 hash of key
func(k string) ([]byte, error) {
hash := sha256.Sum256([]byte(k))
return hash[:], nil
},
// Method 2: Key as-is (padded/truncated to 32 bytes)
func(k string) ([]byte, error) {
keyBytes := []byte(k)
if len(keyBytes) < 32 {
// Pad with zeros
padded := make([]byte, 32)
copy(padded, keyBytes)
return padded, nil
}
return keyBytes[:32], nil
},
// Method 3: MD5 hash repeated to make 32 bytes
func(k string) ([]byte, error) {
hash := md5.Sum([]byte(k))
key32 := make([]byte, 32)
copy(key32[:16], hash[:])
copy(key32[16:], hash[:])
return key32, nil
},
}
for keyIdx, keyMethod := range keyMethods {
keyBytes, err := keyMethod(key)
if err != nil {
continue
}
block, err := aes.NewCipher(keyBytes)
if err != nil {
continue
}
// Try different IV methods for each key method
ivMethods := []func([]byte, []byte) (string, error){
// IV from key hash
func(ct, kb []byte) (string, error) {
hash := sha256.Sum256(kb)
return tryDecryptWithCustomIV(ct, block, hash[:aes.BlockSize])
},
// IV from ciphertext
func(ct, kb []byte) (string, error) {
return tryDecryptWithCipherIV(ct, block)
},
// Zero IV
func(ct, kb []byte) (string, error) {
iv := make([]byte, aes.BlockSize)
return tryDecryptWithCustomIV(ct, block, iv)
},
// IV from key directly
func(ct, kb []byte) (string, error) {
iv := make([]byte, aes.BlockSize)
copy(iv, kb[:aes.BlockSize])
return tryDecryptWithCustomIV(ct, block, iv)
},
}
for ivIdx, ivMethod := range ivMethods {
if result, err := ivMethod(cipherText, keyBytes); err == nil {
log.Printf("ResponseVclaim: Success with key method %d, IV method %d", keyIdx+1, ivIdx+1)
log.Printf("ResponseVclaim result preview: %s", result[:min(200, len(result))])
return result, nil
} else {
log.Printf("ResponseVclaim: Key method %d, IV method %d failed: %v", keyIdx+1, ivIdx+1, err)
}
}
}
return "", errors.New("all decryption methods failed")
}
// func ResponseVclaim(encrypted string, key string) (string, error) {
// log.Println("ResponseVclaim: Starting decryption process")
// log.Printf("ResponseVclaim: Encrypted string length: %d", len(encrypted))
// // Pad the base64 string if needed
// if len(encrypted)%4 != 0 {
// padding := (4 - len(encrypted)%4) % 4
// for i := 0; i < padding; i++ {
// encrypted += "="
// }
// }
// // Decode base64
// cipherText, err := base64.StdEncoding.DecodeString(encrypted)
// if err != nil {
// log.Printf("ResponseVclaim: Failed to decode base64: %v", err)
// return "", err
// }
// if len(cipherText) < aes.BlockSize {
// return "", errors.New("cipherText too short")
// }
// // Create AES cipher
// hash := sha256.Sum256([]byte(key))
// block, err := aes.NewCipher(hash[:])
// if err != nil {
// return "", err
// }
// // Try multiple decryption methods
// methods := []func([]byte, cipher.Block, []byte) (string, error){
// // Method 1: IV from hash (current approach)
// func(ct []byte, b cipher.Block, h []byte) (string, error) {
// return tryDecryptWithHashIV(ct, b, h[:aes.BlockSize])
// },
// // Method 2: IV from cipherText (standard approach)
// func(ct []byte, b cipher.Block, h []byte) (string, error) {
// return tryDecryptWithCipherIV(ct, b)
// },
// // Method 3: Try without padding removal
// func(ct []byte, b cipher.Block, h []byte) (string, error) {
// return tryDecryptWithoutPaddingRemoval(ct, b, h[:aes.BlockSize])
// },
// }
// for i, method := range methods {
// if result, err := method(cipherText, block, hash[:]); err == nil {
// log.Printf("ResponseVclaim: Success with method %d", i+1)
// log.Printf("ResponseVclaim result: %s", result[:min(100, len(result))])
// return result, nil
// } else {
// log.Printf("ResponseVclaim: Method %d failed: %v", i+1, err)
// }
// }
// return "", errors.New("all decryption methods failed")
// }
// func ResponseVclaim(encrypted string, key string) (string, error) {
// log.Println("ResponseVclaim: Starting decryption process")
// log.Printf("ResponseVclaim: Encrypted string length: %d", len(encrypted))
// // Pad the base64 string if needed
// if len(encrypted)%4 != 0 {
// padding := (4 - len(encrypted)%4) % 4
// for i := 0; i < padding; i++ {
// encrypted += "="
// }
// }
// // Decode base64
// cipherText, err := base64.StdEncoding.DecodeString(encrypted)
// if err != nil {
// log.Printf("ResponseVclaim: Failed to decode base64: %v", err)
// return "", err
// }
// if len(cipherText) < aes.BlockSize {
// return "", errors.New("cipherText too short")
// }
// // Create AES cipher
// hash := sha256.Sum256([]byte(key))
// block, err := aes.NewCipher(hash[:])
// if err != nil {
// return "", err
// }
// // Try both IV methods
// // Method 1: IV from hash (current approach)
// if result, err := tryDecryptWithHashIV(cipherText, block, hash[:aes.BlockSize]); err == nil {
// log.Printf("ResponseVclaim: Success with hash IV method")
// log.Println("ResponseVclaim: ", result)
// return result, nil
// }
// // Method 2: IV from cipherText (standard approach)
// if result, err := tryDecryptWithCipherIV(cipherText, block); err == nil {
// log.Printf("ResponseVclaim: Success with cipher IV method")
// return result, nil
// }
// return "", errors.New("all decryption methods failed")
// }
// func tryDecryptWithHashIV(cipherText []byte, block cipher.Block, iv []byte) (string, error) {
// if len(cipherText)%aes.BlockSize != 0 {
// return "", errors.New("cipherText is not a multiple of the block size")
// }
// mode := cipher.NewCBCDecrypter(block, iv)
// decrypted := make([]byte, len(cipherText))
// mode.CryptBlocks(decrypted, cipherText)
// // Remove PKCS7 padding
// decrypted = helper.RemovePKCS7Padding(decrypted)
// log.Printf("tryDecryptWithHashIV: Decryption completed, length: %d", len(decrypted))
// return tryAllDecompressionMethods(decrypted)
// }
func tryDecryptWithHashIV(cipherText []byte, block cipher.Block, iv []byte) (string, error) {
if len(cipherText)%aes.BlockSize != 0 {
return "", errors.New("cipherText is not a multiple of the block size")
}
mode := cipher.NewCBCDecrypter(block, iv)
decrypted := make([]byte, len(cipherText))
mode.CryptBlocks(decrypted, cipherText)
// Log raw decrypted data before padding removal
log.Printf("tryDecryptWithHashIV: Raw decrypted length: %d", len(decrypted))
// Remove PKCS7 padding
decrypted = helper.RemovePKCS7Padding(decrypted)
log.Printf("tryDecryptWithHashIV: After padding removal, length: %d", len(decrypted))
// Log first 50 bytes untuk debugging
logLen := min(50, len(decrypted))
log.Printf("tryDecryptWithHashIV: First %d bytes: %q", logLen, string(decrypted[:logLen]))
return tryAllDecompressionMethods(decrypted)
}
// func tryDecryptWithCipherIV(cipherText []byte, block cipher.Block) (string, error) {
// if len(cipherText) < aes.BlockSize {
// return "", errors.New("cipherText too short for IV extraction")
// }
// // Extract IV from first block
// iv := cipherText[:aes.BlockSize]
// cipherData := cipherText[aes.BlockSize:]
// if len(cipherData)%aes.BlockSize != 0 {
// return "", errors.New("cipher data is not a multiple of the block size")
// }
// mode := cipher.NewCBCDecrypter(block, iv)
// decrypted := make([]byte, len(cipherData))
// mode.CryptBlocks(decrypted, cipherData)
// // Remove PKCS7 padding
// decrypted = helper.RemovePKCS7Padding(decrypted)
// log.Printf("tryDecryptWithCipherIV: Decryption completed, length: %d", len(decrypted))
// return tryAllDecompressionMethods(decrypted)
// }
func tryDecryptWithCipherIV(cipherText []byte, block cipher.Block) (string, error) {
if len(cipherText) < aes.BlockSize {
return "", errors.New("cipherText too short for IV extraction")
}
// Extract IV from first block
iv := cipherText[:aes.BlockSize]
cipherData := cipherText[aes.BlockSize:]
if len(cipherData)%aes.BlockSize != 0 {
return "", errors.New("cipher data is not a multiple of the block size")
}
mode := cipher.NewCBCDecrypter(block, iv)
decrypted := make([]byte, len(cipherData))
mode.CryptBlocks(decrypted, cipherData)
// Log raw decrypted data before padding removal
log.Printf("tryDecryptWithCipherIV: Raw decrypted length: %d", len(decrypted))
// Remove PKCS7 padding
decrypted = helper.RemovePKCS7Padding(decrypted)
log.Printf("tryDecryptWithCipherIV: After padding removal, length: %d", len(decrypted))
// Log first 50 bytes untuk debugging
logLen := min(50, len(decrypted))
log.Printf("tryDecryptWithCipherIV: First %d bytes: %q", logLen, string(decrypted[:logLen]))
return tryAllDecompressionMethods(decrypted)
}
func tryAllDecompressionMethods(data []byte) (string, error) {
log.Printf("tryAllDecompressionMethods: Attempting decompression, data length: %d", len(data))
// Log hex dump for better debugging
hexDump := make([]string, min(32, len(data)))
for i := 0; i < len(hexDump); i++ {
hexDump[i] = fmt.Sprintf("%02x", data[i])
}
log.Printf("tryAllDecompressionMethods: Hex dump (first 32 bytes): %s", strings.Join(hexDump, " "))
// Method 1: Try LZ-string first (most common for BPJS)
if result, err := tryLZStringMethods(data); err == nil {
log.Println("tryAllDecompressionMethods: LZ-string decompression successful")
return result, nil
}
// Method 2: Try gzip
if result, err := tryGzipDecompression(data); err == nil && isValidDecompressedResult(result) {
log.Println("tryAllDecompressionMethods: Gzip decompression successful")
return result, nil
}
// Method 3: Try as plain text
if isValidUTF8AndPrintable(string(data)) {
result := string(data)
if isValidDecompressedResult(result) {
log.Println("tryAllDecompressionMethods: Data is already valid text")
return result, nil
}
}
// Method 4: Try base64 decode then decompress
if result, err := tryBase64ThenDecompress(data); err == nil {
log.Println("tryAllDecompressionMethods: Base64 then decompress successful")
return result, nil
}
log.Printf("tryAllDecompressionMethods: All methods failed")
return "", errors.New("all decompression methods failed")
}
// func tryAllDecompressionMethods(data []byte) (string, error) {
// log.Printf("tryAllDecompressionMethods: Attempting decompression, data length: %d", len(data))
// // Method 1: Try LZ-string decompression FIRST (paling umum untuk BPJS API)
// if result, err := tryLZStringMethods(data); err == nil {
// log.Println("tryAllDecompressionMethods: LZ-string decompression successful")
// return result, nil
// }
// // Method 2: Try gzip decompression
// if result, err := tryGzipDecompression(data); err == nil && isValidDecompressedResult(result) {
// log.Println("tryAllDecompressionMethods: Gzip decompression successful")
// return result, nil
// }
// // Method 3: Check if it's already valid JSON/text (SETELAH mencoba decompression)
// if isValidUTF8AndPrintable(string(data)) {
// result := string(data)
// if isValidDecompressedResult(result) {
// log.Println("tryAllDecompressionMethods: Data is already valid, returning as-is")
// return result, nil
// }
// }
// // Method 4: Log the raw data for debugging
// log.Printf("tryAllDecompressionMethods: All methods failed. Raw data (first 100 bytes): %q", string(data[:min(100, len(data))]))
// return "", errors.New("all decompression methods failed")
// }
// func tryLZStringMethods(data []byte) (string, error) {
// dataStr := string(data)
// log.Printf("tryLZStringMethods: Attempting LZ-string decompression on data: %s", dataStr[:min(50, len(dataStr))])
// // Method 1: DecompressFromEncodedURIComponent (paling umum)
// if result, err := lzstring.DecompressFromEncodedURIComponent(dataStr); err == nil && len(result) > 0 {
// log.Printf("LZ-string DecompressFromEncodedURIComponent attempted, result length: %d", len(result))
// if isValidDecompressedResult(result) {
// log.Printf("LZ-string DecompressFromEncodedURIComponent successful and valid")
// return result, nil
// } else {
// log.Printf("LZ-string DecompressFromEncodedURIComponent result not valid: %s", result[:min(100, len(result))])
// }
// }
// // Method 2: DecompressFromBase64
// if result, err := lzstring.DecompressFromBase64(dataStr); err == nil && len(result) > 0 {
// log.Printf("LZ-string DecompressFromBase64 attempted, result length: %d", len(result))
// if isValidDecompressedResult(result) {
// log.Printf("LZ-string DecompressFromBase64 successful and valid")
// return result, nil
// } else {
// log.Printf("LZ-string DecompressFromBase64 result not valid: %s", result[:min(100, len(result))])
// }
// }
// // Method 3: Try base64 decode first, then decompress
// if decoded, err := base64.StdEncoding.DecodeString(dataStr); err == nil {
// if result, err := lzstring.DecompressFromEncodedURIComponent(string(decoded)); err == nil && len(result) > 0 {
// if isValidDecompressedResult(result) {
// log.Printf("LZ-string with base64 decode first successful and valid")
// return result, nil
// }
// }
// }
// // Method 4: Try as raw bytes (convert each byte to uint16)
// if result, err := tryRawBytesToLZString(data); err == nil && len(result) > 0 {
// if isValidDecompressedResult(result) {
// log.Printf("LZ-string from raw bytes successful and valid")
// return result, nil
// }
// }
// log.Printf("All LZ-string methods failed or returned invalid results")
// return "", errors.New("all LZ-string methods failed")
// }
func tryLZStringMethods(data []byte) (string, error) {
dataStr := string(data)
log.Printf("tryLZStringMethods: Raw data length: %d", len(dataStr))
// Method 1: Bersihkan prefix corrupt dan cari pattern LZ-string
cleanedData := extractCleanLZString(dataStr)
if cleanedData != "" {
log.Printf("tryLZStringMethods: Found clean LZ-string: %s", cleanedData[:min(50, len(cleanedData))])
// Dekompresi sesuai standar BPJS
if result, err := lzstring.DecompressFromEncodedURIComponent(cleanedData); err == nil && len(result) > 0 {
if isValidDecompressedResult(result) {
log.Printf("LZ-string decompression successful, length: %d", len(result))
return result, nil
}
}
}
// Method 2: Fallback direct decompression
if result, err := lzstring.DecompressFromEncodedURIComponent(dataStr); err == nil && len(result) > 0 {
if isValidDecompressedResult(result) {
return result, nil
}
}
return "", errors.New("LZ-string decompression failed")
}
func extractCleanLZString(data string) string {
// Pattern LZ-string umum dari dokumentasi BPJS
patterns := []string{"EAuUA", "N4Ig", "BwIw", "CwIw", "DwIw", "EwIw", "FwIw", "GwIw", "HwIw"}
for _, pattern := range patterns {
if idx := strings.Index(data, pattern); idx >= 0 {
// Ekstrak dari pattern hingga akhir
candidate := data[idx:]
log.Printf("extractCleanLZString: Found pattern '%s' at position %d", pattern, idx)
// Bersihkan hanya karakter base64 valid
cleaned := extractBase64Only(candidate)
if len(cleaned) > 100 { // Minimal length untuk data valid
return cleaned
}
}
}
return ""
}
// Fungsi untuk mengekstrak hanya karakter base64 valid
func extractBase64Only(s string) string {
base64Chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
var result strings.Builder
for _, char := range s {
if strings.ContainsRune(base64Chars, char) {
result.WriteRune(char)
} else {
// Stop di karakter non-base64 jika sudah cukup panjang
if result.Len() > 100 {
break
}
}
}
return result.String()
}
// Fungsi untuk mencari dan mengekstrak LZ-string dari data yang mungkin corrupt
func findAndExtractLZString(data string) string {
// Pattern LZ-string umum - prioritaskan yang paling umum
patterns := []string{
"EAuUA", "N4Ig", "BwIw", "CwIw", "DwIw", "EwIw",
"FwIw", "GwIw", "HwIw", "BAuUA", "CAuUA", "DAuUA", "AAuUA",
}
// Cari pattern yang paling awal dalam string
earliestIdx := -1
bestPattern := ""
for _, pattern := range patterns {
if idx := strings.Index(data, pattern); idx >= 0 {
if earliestIdx == -1 || idx < earliestIdx {
earliestIdx = idx
bestPattern = pattern
}
}
}
if earliestIdx >= 0 {
// Ekstrak dari posisi pattern hingga akhir
candidate := data[earliestIdx:]
log.Printf("findAndExtractLZString: Found pattern '%s' at position %d", bestPattern, earliestIdx)
log.Printf("findAndExtractLZString: Extracted data: %s", candidate[:min(100, len(candidate))])
// Bersihkan dari karakter non-base64 di akhir
cleaned := cleanBase64String(candidate)
if len(cleaned) > 50 {
log.Printf("findAndExtractLZString: Cleaned data length: %d", len(cleaned))
return cleaned
}
}
// Fallback: cari sequence base64 terpanjang
return extractLongestBase64Sequence(data)
}
// Fungsi untuk membersihkan string hingga hanya karakter base64 valid
func cleanBase64String(s string) string {
base64Chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
var result strings.Builder
for _, char := range s {
if strings.ContainsRune(base64Chars, char) {
result.WriteRune(char)
} else {
// Jika bertemu karakter non-base64 dan sudah cukup panjang, stop
if result.Len() > 100 {
break
}
// Skip karakter non-base64 di awal
}
}
cleaned := result.String()
log.Printf("cleanBase64String: Original length: %d, Cleaned length: %d", len(s), len(cleaned))
return cleaned
}
// // Fungsi untuk membersihkan string hingga hanya karakter base64 valid
// func cleanBase64String(s string) string {
// base64Chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
// var result strings.Builder
// for _, char := range s {
// if strings.ContainsRune(base64Chars, char) {
// result.WriteRune(char)
// } else if result.Len() > 50 {
// // Stop jika sudah cukup panjang dan bertemu karakter non-base64
// break
// }
// }
// return result.String()
// }
// Fungsi untuk membersihkan karakter non-printable
func cleanNonPrintableChars(s string) string {
var result strings.Builder
for _, r := range s {
if r >= 32 && r <= 126 || r == '\n' || r == '\r' || r == '\t' {
result.WriteRune(r)
}
}
return result.String()
}
// Fungsi baru untuk mengekstrak data LZ-string yang bersih dari data corrupt
func extractLZStringFromCorruptData(data string) string {
// Cari pattern LZ-string yang umum
patterns := []string{"N4Ig", "BwIw", "CwIw", "DwIw", "EwIw", "FwIw", "GwIw", "HwIw", "EAuUA"}
for _, pattern := range patterns {
if idx := strings.Index(data, pattern); idx > 0 {
cleanData := data[idx:]
log.Printf("extractLZStringFromCorruptData: Found pattern '%s' at position %d", pattern, idx)
// Validasi bahwa data setelah pattern adalah base64-like characters
if isBase64Like(cleanData) {
return cleanData
}
}
}
// Jika tidak ada pattern yang ditemukan, coba cari sequences panjang dari base64 characters
return extractLongestBase64Sequence(data)
}
// Fungsi untuk mengecek apakah string berisi karakter base64
func isBase64Like(s string) bool {
if len(s) < 20 {
return false
}
base64Chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
validCount := 0
for i, char := range s[:min(100, len(s))] {
if strings.ContainsRune(base64Chars, char) {
validCount++
}
if i > 20 && float64(validCount)/float64(i+1) < 0.8 {
return false
}
}
return float64(validCount)/float64(min(100, len(s))) >= 0.8
}
// Fungsi untuk mengekstrak sequence base64 terpanjang
func extractLongestBase64Sequence(data string) string {
base64Chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
bestStart := -1
bestLength := 0
currentStart := -1
currentLength := 0
for i, char := range data {
if strings.ContainsRune(base64Chars, char) {
if currentStart == -1 {
currentStart = i
currentLength = 1
} else {
currentLength++
}
} else {
if currentLength > bestLength && currentLength > 50 {
bestStart = currentStart
bestLength = currentLength
}
currentStart = -1
currentLength = 0
}
}
// Check final sequence
if currentLength > bestLength && currentLength > 50 {
bestStart = currentStart
bestLength = currentLength
}
if bestStart >= 0 && bestLength > 50 {
result := data[bestStart : bestStart+bestLength]
log.Printf("extractLongestBase64Sequence: Found sequence at pos %d, length %d", bestStart, bestLength)
return result
}
return ""
}
// Fungsi untuk validasi hasil dekompresi
func isValidDecompressedResult(result string) bool {
if len(result) == 0 {
return false
}
// Trim whitespace dan cek UTF-8
trimmed := strings.TrimSpace(result)
if !utf8.ValidString(trimmed) {
return false
}
// Harus dimulai dengan { atau [ untuk JSON
if len(trimmed) > 0 && (trimmed[0] == '{' || trimmed[0] == '[') {
// Validasi sebagai JSON
var js json.RawMessage
if json.Unmarshal([]byte(result), &js) == nil {
log.Printf("Decompressed result is valid JSON, length: %d", len(result))
return true
}
}
// Jika bukan JSON, tolak
log.Printf("Decompressed result is not valid JSON")
return false
}
// func isValidDecompressedResult(result string) bool {
// if len(result) == 0 {
// return false
// }
// // Check if result contains only printable ASCII and valid UTF-8
// if !isValidUTF8AndPrintable(result) {
// log.Printf("Decompressed result contains invalid characters")
// return false
// }
// // Check if it looks like JSON (starts with { or [)
// trimmed := strings.TrimSpace(result)
// if len(trimmed) > 0 && (trimmed[0] == '{' || trimmed[0] == '[') {
// // Try to validate as JSON
// var js json.RawMessage
// if json.Unmarshal([]byte(result), &js) == nil {
// log.Printf("Decompressed result is valid JSON")
// return true
// }
// }
// // PERBAIKAN: Jangan anggap data yang dimulai dengan karakter tertentu sebagai valid text
// // Data LZ-string biasanya dimulai dengan karakter seperti N4Ig, BwIw, dll
// if detectLZStringPattern(result) {
// log.Printf("Data appears to be LZ-string compressed, needs decompression")
// return false
// }
// // If not JSON, check if it's reasonable text content
// if len(result) > 10 && isReasonableTextContent(result) {
// log.Printf("Decompressed result appears to be valid text content")
// return true
// }
// return false
// }
// Fungsi baru untuk mendeteksi pola LZ-string
func detectLZStringPattern(s string) bool {
if len(s) < 10 {
return false
}
// Pattern umum LZ-string compressed data
commonLZPatterns := []string{
"N4Ig", "BwIw", "CwIw", "DwIw", "EwIw", "FwIw", "GwIw", "HwIw",
"IwIw", "JwIw", "KwIw", "LwIw", "MwIw", "NwIw", "OwIw", "PwIw",
}
for _, pattern := range commonLZPatterns {
if strings.HasPrefix(s, pattern) {
return true
}
}
// Cek apakah string hanya berisi karakter base64 tanpa spasi atau newline
base64Pattern := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
if len(s) > 50 { // Only check long strings
invalidChars := 0
for _, char := range s {
if !strings.ContainsRune(base64Pattern, char) {
invalidChars++
}
}
// Jika kurang dari 5% karakter invalid, kemungkinan ini LZ-string
if float64(invalidChars)/float64(len(s)) < 0.05 {
return true
}
}
return false
}
// func isValidUTF8AndPrintable(s string) bool {
// if !utf8.ValidString(s) {
// return false
// }
// // Allow common characters: letters, numbers, spaces, and common JSON characters
// for _, r := range s {
// if r < 32 && r != '\n' && r != '\r' && r != '\t' {
// return false // Control characters (except newline, carriage return, tab)
// }
// if r > 126 && r < 160 {
// return false // Extended ASCII control characters
// }
// // Allow Unicode characters above 160
// }
// return true
// }
func isValidUTF8AndPrintable(s string) bool {
if !utf8.ValidString(s) {
log.Printf("isValidUTF8AndPrintable: String is not valid UTF-8")
return false
}
// Hitung karakter yang valid
validChars := 0
totalChars := 0
for _, r := range s {
totalChars++
if r >= 32 && r <= 126 { // Printable ASCII
validChars++
} else if r == '\n' || r == '\r' || r == '\t' { // Allowed control chars
validChars++
} else if r >= 160 { // Unicode characters
validChars++
}
}
validRatio := float64(validChars) / float64(totalChars)
log.Printf("isValidUTF8AndPrintable: Valid chars ratio: %.2f (%d/%d)", validRatio, validChars, totalChars)
// At least 70% should be valid characters
return validRatio >= 0.7
}
func isReasonableTextContent(s string) bool {
// Count printable characters
printableCount := 0
for _, r := range s {
if unicode.IsPrint(r) || unicode.IsSpace(r) {
printableCount++
}
}
// At least 80% should be printable
return float64(printableCount)/float64(len([]rune(s))) >= 0.8
}
// Perbaikan untuk konversi raw bytes ke LZ-string
func tryRawBytesToLZString(data []byte) (string, error) {
// Convert bytes to uint16 array (for UTF-16 decompression)
if len(data)%2 != 0 {
// Pad with zero if odd length
data = append(data, 0)
}
utf16Data := make([]uint16, len(data)/2)
for i := 0; i < len(data); i += 2 {
utf16Data[i/2] = uint16(data[i]) | (uint16(data[i+1]) << 8)
}
return lzstring.DecompressFromUTF16(utf16Data)
}
func isValidJSON(data []byte) bool {
if len(data) == 0 {
return false
}
firstChar := data[0]
return firstChar == '{' || firstChar == '['
}
func tryGzipDecompression(data []byte) (string, error) {
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return "", err
}
defer reader.Close()
decompressed, err := io.ReadAll(reader)
if err != nil {
return "", err
}
return string(decompressed), nil
}
// stringToUTF16 converts string to []uint16 for UTF16 decompression
func stringToUTF16(s string) ([]uint16, error) {
if len(s) == 0 {
return nil, errors.New("empty string")
}
// Convert string to runes first
runes := []rune(s)
// Convert runes to UTF16
utf16Data := utf16.Encode(runes)
return utf16Data, nil
}
// Method dekripsi tanpa padding removal
func tryDecryptWithoutPaddingRemoval(cipherText []byte, block cipher.Block, iv []byte) (string, error) {
if len(cipherText)%aes.BlockSize != 0 {
return "", errors.New("cipherText is not a multiple of the block size")
}
mode := cipher.NewCBCDecrypter(block, iv)
decrypted := make([]byte, len(cipherText))
mode.CryptBlocks(decrypted, cipherText)
log.Printf("tryDecryptWithoutPaddingRemoval: Decrypted length: %d", len(decrypted))
// Coba tanpa remove padding dulu
return tryAllDecompressionMethods(decrypted)
}
func tryDecryptWithCustomIV(cipherText []byte, block cipher.Block, iv []byte) (string, error) {
if len(cipherText)%aes.BlockSize != 0 {
return "", errors.New("cipherText is not a multiple of the block size")
}
mode := cipher.NewCBCDecrypter(block, iv)
decrypted := make([]byte, len(cipherText))
mode.CryptBlocks(decrypted, cipherText)
log.Printf("tryDecryptWithCustomIV: Raw decrypted length: %d", len(decrypted))
log.Printf("tryDecryptWithCustomIV: Raw first 50 bytes: %q", string(decrypted[:min(50, len(decrypted))]))
// Try multiple padding removal strategies
paddingStrategies := []func([]byte) []byte{
helper.RemovePKCS7Padding,
removePaddingManual,
func(data []byte) []byte { return data }, // No padding removal
}
for i, strategy := range paddingStrategies {
processed := strategy(decrypted)
log.Printf("tryDecryptWithCustomIV: Strategy %d, processed length: %d", i+1, len(processed))
if result, err := tryAllDecompressionMethods(processed); err == nil {
log.Printf("tryDecryptWithCustomIV: Success with padding strategy %d", i+1)
return result, nil
}
}
return "", errors.New("all padding strategies failed")
}
// Manual padding removal yang lebih agresif
func removePaddingManual(data []byte) []byte {
if len(data) == 0 {
return data
}
// Coba berbagai kemungkinan padding
for padLen := 1; padLen <= min(16, len(data)); padLen++ {
if data[len(data)-1] == byte(padLen) {
// Check if all padding bytes match
valid := true
start := len(data) - padLen
for i := start; i < len(data); i++ {
if data[i] != byte(padLen) {
valid = false
break
}
}
if valid {
log.Printf("removePaddingManual: Found valid padding of length %d", padLen)
return data[:start]
}
}
}
// Jika tidak ada padding yang valid, coba buang beberapa byte terakhir
for i := 1; i <= min(16, len(data)); i++ {
trimmed := data[:len(data)-i]
if isLikelyValidData(trimmed) {
log.Printf("removePaddingManual: Trimmed %d bytes, result seems valid", i)
return trimmed
}
}
return data
}
// Fungsi untuk mengecek apakah data kemungkinan valid
func isLikelyValidData(data []byte) bool {
if len(data) < 10 {
return false
}
// Check for common patterns in compressed data or JSON
str := string(data)
// LZ-string patterns
if strings.HasPrefix(str, "N4Ig") || strings.HasPrefix(str, "BwIw") {
return true
}
// JSON patterns
if strings.HasPrefix(str, "{") || strings.HasPrefix(str, "[") {
return true
}
// Gzip magic number
if len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b {
return true
}
return false
}
func tryBase64ThenDecompress(data []byte) (string, error) {
decoded, err := base64.StdEncoding.DecodeString(string(data))
if err != nil {
return "", err
}
return tryLZStringMethods(decoded)
}
// Fungsi alternatif untuk menghapus prefix corrupt dari data
func removeCorruptPrefix(data string) string {
// Cari pattern LZ-string yang dikenal
patterns := []string{"EAuUA", "N4Ig", "BwIw", "BAuUA"}
for _, pattern := range patterns {
if idx := strings.Index(data, pattern); idx >= 0 {
cleaned := data[idx:]
log.Printf("removeCorruptPrefix: Removed %d corrupt bytes, found pattern: %s", idx, pattern)
return cleaned
}
}
// Jika tidak ada pattern ditemukan, coba hapus karakter non-printable di awal
var result strings.Builder
started := false
for _, r := range data {
if !started {
// Mulai mengumpulkan karakter setelah bertemu karakter valid
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '+' || r == '/' || r == '=' {
started = true
result.WriteRune(r)
}
} else {
result.WriteRune(r)
}
}
return result.String()
}