From 87b69ddb298aaf52513766e13ac31e9bdf3b7d92 Mon Sep 17 00:00:00 2001 From: Meninjar Date: Wed, 10 Sep 2025 21:31:31 +0700 Subject: [PATCH] perbaikan response --- internal/helpers/bpjs/lz-string.go | 45 +- internal/services/bpjs/response.go | 989 ++++++++++++++++-- internal/services/bpjs/vclaimBridge.go | 124 ++- .../bpjs/services-config-bpjs.yaml | 8 +- .../satusehat/services-config-satusehat.yaml | 0 5 files changed, 1079 insertions(+), 87 deletions(-) rename services-config-bpjs.yaml => tools/bpjs/services-config-bpjs.yaml (96%) rename services-config-satusehat.yaml => tools/satusehat/services-config-satusehat.yaml (100%) diff --git a/internal/helpers/bpjs/lz-string.go b/internal/helpers/bpjs/lz-string.go index d00882f6..c05dd715 100644 --- a/internal/helpers/bpjs/lz-string.go +++ b/internal/helpers/bpjs/lz-string.go @@ -33,18 +33,51 @@ func RemovePKCS7Padding(data []byte) []byte { } paddingLength := int(data[len(data)-1]) - if paddingLength > len(data) || paddingLength == 0 { + + // Validasi padding length + if paddingLength > len(data) || paddingLength == 0 || paddingLength > 16 { log.Printf("RemovePKCS7Padding: Invalid padding length: %d, data length: %d", paddingLength, len(data)) - return data // Return original data if padding is invalid + // Coba kembalikan data tanpa byte terakhir jika padding tampak salah + if len(data) > 1 { + return data[:len(data)-1] + } + return data } // Verify all padding bytes are correct - for i := len(data) - paddingLength; i < len(data); i++ { + paddingStart := len(data) - paddingLength + for i := paddingStart; i < len(data); i++ { if data[i] != byte(paddingLength) { - log.Printf("RemovePKCS7Padding: Invalid padding byte at position %d", i) - return data // Return original data if padding is invalid + log.Printf("RemovePKCS7Padding: Invalid padding byte at position %d, expected %d, got %d", i, paddingLength, data[i]) + // Jika padding tidak valid, coba cari padding yang benar + return findValidPadding(data) } } - return data[:len(data)-paddingLength] + result := data[:paddingStart] + log.Printf("RemovePKCS7Padding: Successfully removed %d padding bytes", paddingLength) + return result +} + +// Fungsi baru untuk mencari padding yang valid +func findValidPadding(data []byte) []byte { + dataLen := len(data) + + // Coba berbagai kemungkinan padding length (1-16) + for padLen := 1; padLen <= 16 && padLen <= dataLen; padLen++ { + valid := true + for i := dataLen - padLen; i < dataLen; i++ { + if data[i] != byte(padLen) { + valid = false + break + } + } + if valid { + log.Printf("RemovePKCS7Padding: Found valid padding of length %d", padLen) + return data[:dataLen-padLen] + } + } + + log.Printf("RemovePKCS7Padding: No valid padding found, returning original data") + return data } diff --git a/internal/services/bpjs/response.go b/internal/services/bpjs/response.go index d05cd43e..97ce46c4 100644 --- a/internal/services/bpjs/response.go +++ b/internal/services/bpjs/response.go @@ -6,12 +6,18 @@ import ( "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" ) @@ -23,7 +29,27 @@ func min(a, b int) int { 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)) @@ -47,29 +73,204 @@ func ResponseVclaim(encrypted string, key string) (string, error) { 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 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 + }, } - // 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") - return result, nil - } + for keyIdx, keyMethod := range keyMethods { + keyBytes, err := keyMethod(key) + if err != nil { + continue + } - // 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 + 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") @@ -79,13 +280,43 @@ func tryDecryptWithHashIV(cipherText []byte, block cipher.Block, iv []byte) (str 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: Decryption completed, length: %d", len(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") @@ -103,44 +334,552 @@ func tryDecryptWithCipherIV(cipherText []byte, block cipher.Block) (string, erro 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: Decryption completed, length: %d", len(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)) - // Method 1: Check if it's already valid JSON - if isValidJSON(data) { - log.Println("tryAllDecompressionMethods: Data is valid JSON, returning as-is") - return string(data), nil + // 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 2: Try gzip decompression - if result, err := tryGzipDecompression(data); err == nil && len(result) > 0 { - log.Println("tryAllDecompressionMethods: Gzip decompression successful") - return result, nil - } - - // Method 3: Try LZ-string decompression methods - if result, err := tryLZStringMethods(data); err == nil && len(result) > 0 { + // 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 4: Return as plain text - result := string(data) - if len(result) > 0 { - log.Printf("tryAllDecompressionMethods: Using decrypted data as plain text, length: %d", len(result)) + // 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 @@ -164,36 +903,6 @@ func tryGzipDecompression(data []byte) (string, error) { return string(decompressed), nil } -func tryLZStringMethods(data []byte) (string, error) { - dataStr := string(data) - - // Method 1: DecompressFromEncodedURIComponent - if result, err := lzstring.DecompressFromEncodedURIComponent(dataStr); err == nil && len(result) > 0 { - return result, nil - } - - // Method 2: DecompressFromBase64 - if result, err := lzstring.DecompressFromBase64(dataStr); err == nil && len(result) > 0 { - return result, nil - } - - // Method 3: DecompressFromUTF16 (with proper conversion) - if utf16Data, err := stringToUTF16(dataStr); err == nil { - if result, err := lzstring.DecompressFromUTF16(utf16Data); err == nil && len(result) > 0 { - return result, nil - } - } - - // Method 4: Try with base64 decoding first - if decoded, err := base64.StdEncoding.DecodeString(dataStr); err == nil { - if result, err := lzstring.DecompressFromEncodedURIComponent(string(decoded)); err == nil && len(result) > 0 { - return result, nil - } - } - - return "", errors.New("all LZ-string methods failed") -} - // stringToUTF16 converts string to []uint16 for UTF16 decompression func stringToUTF16(s string) ([]uint16, error) { if len(s) == 0 { @@ -208,3 +917,155 @@ func stringToUTF16(s string) ([]uint16, error) { 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() +} diff --git a/internal/services/bpjs/vclaimBridge.go b/internal/services/bpjs/vclaimBridge.go index a3ad8a19..90f1f38d 100644 --- a/internal/services/bpjs/vclaimBridge.go +++ b/internal/services/bpjs/vclaimBridge.go @@ -9,6 +9,7 @@ import ( "net/http" "strings" "time" + "unicode" "api-service/internal/config" "api-service/internal/models/vclaim/peserta" @@ -18,16 +19,6 @@ import ( ) // cleanResponse removes invalid characters and BOM from the response string -func cleanResponse(resp string) string { - // Remove UTF-8 BOM - resp = strings.TrimPrefix(resp, "\xef\xbb\xbf") - resp = strings.TrimPrefix(resp, "\ufeff") - // Remove null characters - resp = strings.ReplaceAll(resp, "\x00", "") - // Trim whitespace - resp = strings.TrimSpace(resp) - return resp -} // VClaimService interface for VClaim operations type VClaimService interface { @@ -201,6 +192,17 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error) Int("key_length", len(decryptionKey)). Msg("Decryption key components") + // // Decrypt response + // consID, secretKey, userKey, tstamp, _ := s.config.SetHeader() + // decryptionKey := GenerateBPJSKey(consID, tstamp, secretKey) // Menggunakan fungsi baru + // log.Debug(). + // Str("consID", consID). + // Str("tstamp", tstamp). + // Str("userKey", userKey). + // Str("secretKey", secretKey). + // Int("key_length", len(decryptionKey)). + // Msg("Decryption key components") + respDecrypt, err := ResponseVclaim(respMentah.Response, decryptionKey) if err != nil { log.Error().Err(err).Msg("Failed to decrypt response") @@ -211,9 +213,28 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error) if respDecrypt != "" { // Clean the decrypted response respDecrypt = cleanResponse(respDecrypt) - if err := json.Unmarshal([]byte(respDecrypt), &finalResp.Response); err != nil { - // If JSON unmarshal fails, store as string - log.Warn().Err(err).Msg("Failed to unmarshal decrypted response, storing as string") + + // Try multiple cleaning strategies + cleaningStrategies := []string{ + respDecrypt, + strings.TrimLeft(respDecrypt, "\ufeff\xfe\xef\xbb\xbf"), + strings.TrimLeftFunc(respDecrypt, func(r rune) bool { return r < 32 && r != '\n' && r != '\r' && r != '\t' }), + } + + var jsonParseSuccess bool + for i, cleaned := range cleaningStrategies { + if err := json.Unmarshal([]byte(cleaned), &finalResp.Response); err == nil { + log.Info(). + Int("strategy", i+1). + Msg("Successfully parsed JSON with cleaning strategy") + jsonParseSuccess = true + break + } + } + + if !jsonParseSuccess { + // If all JSON parsing fails, store as string + log.Warn().Msg("All JSON parsing strategies failed, storing as string") finalResp.Response = respDecrypt } } @@ -477,3 +498,80 @@ func PostRequest(endpoint string, cfg interface{}, data interface{}) interface{} return resp } +func cleanResponse(s string) string { + // Remove UTF-8 BOM dan variasi BOM lainnya + s = strings.TrimPrefix(s, "\xef\xbb\xbf") // UTF-8 BOM + s = strings.TrimPrefix(s, "\ufeff") // Unicode BOM + s = strings.TrimPrefix(s, "\ufffe") // Unicode BOM (reverse) + s = strings.TrimPrefix(s, "\xff\xfe") // UTF-16 LE BOM + s = strings.TrimPrefix(s, "\xfe\xff") // UTF-16 BE BOM + + // Remove karakter control dan non-printable + var result strings.Builder + for _, r := range s { + if r >= 32 && r <= 126 || r == '\n' || r == '\r' || r == '\t' { + result.WriteRune(r) + } else if r > 126 && unicode.IsPrint(r) { + // Allow Unicode printable characters + result.WriteRune(r) + } + // Skip semua karakter lainnya (termasuk BOM fragments) + } + + cleaned := result.String() + cleaned = strings.TrimSpace(cleaned) + + // Cari dan ekstrak JSON yang valid + if idx := strings.Index(cleaned, "{"); idx >= 0 { + cleaned = cleaned[idx:] + // Find matching closing brace + if endIdx := findMatchingBrace(cleaned); endIdx > 0 { + cleaned = cleaned[:endIdx+1] + } + } + + log.Printf("cleanResponse: Final cleaned length: %d", len(cleaned)) + log.Printf("cleanResponse: Final result preview: %s", cleaned[:min(200, len(cleaned))]) + return cleaned +} + +// Fungsi helper untuk menemukan closing brace yang matching +func findMatchingBrace(s string) int { + if len(s) == 0 || s[0] != '{' { + return -1 + } + + braceCount := 0 + inString := false + escaped := false + + for i, char := range s { + if escaped { + escaped = false + continue + } + + if char == '\\' { + escaped = true + continue + } + + if char == '"' && !escaped { + inString = !inString + continue + } + + if !inString { + if char == '{' { + braceCount++ + } else if char == '}' { + braceCount-- + if braceCount == 0 { + return i + } + } + } + } + + return -1 +} diff --git a/services-config-bpjs.yaml b/tools/bpjs/services-config-bpjs.yaml similarity index 96% rename from services-config-bpjs.yaml rename to tools/bpjs/services-config-bpjs.yaml index 9839460b..4dd95e97 100644 --- a/services-config-bpjs.yaml +++ b/tools/bpjs/services-config-bpjs.yaml @@ -24,11 +24,11 @@ services: bynokartu: methods: ["GET"] path: "/peserta/:nokartu" - get_routes: "/Peserta/nokartu/:nokartu" + get_routes: "/nokartu/:nokartu" # post_routes: "/Peserta/nokartu/:nokartu" # put_routes: "/Peserta/nokartu/:nokartu" # delete_routes: "/Peserta/nokartu/:nokartu" - get_path: "/peserta/:nokartu" + get_path: "/Peserta/nokartu/:nokartu/tglSEP/:tglSEP" # post_path: "/peserta" # put_path: "/peserta/:nokartu" # delete_path: "/peserta/:nokartu" @@ -45,11 +45,11 @@ services: bynik: methods: ["GET"] path: "/peserta/nik/:nik" - get_routes: "/Peserta/nik/:nik" + get_routes: "/nik/:nik" # post_routes: "/Peserta/nik/:nik" # put_routes: "/Peserta/nik/:nik" # delete_routes: "/Peserta/nik/:nik" - get_path: "/peserta/nik/:nik" + get_path: "/Peserta/nik/:nik/tglSEP/:tglSEP" # post_path: "/peserta" # put_path: "/peserta/nik/:nik" # delete_path: "/peserta/nik/:nik" diff --git a/services-config-satusehat.yaml b/tools/satusehat/services-config-satusehat.yaml similarity index 100% rename from services-config-satusehat.yaml rename to tools/satusehat/services-config-satusehat.yaml