1072 lines
32 KiB
Go
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()
|
|
}
|