diff --git a/internal/helpers/bpjs/lz-string.go b/internal/helpers/bpjs/lz-string.go index 4e4a706..d00882f 100644 --- a/internal/helpers/bpjs/lz-string.go +++ b/internal/helpers/bpjs/lz-string.go @@ -1,46 +1,50 @@ package helper import ( - "crypto/aes" - "crypto/cipher" - "crypto/sha256" - "encoding/base64" "log" lzstring "github.com/daku10/go-lz-string" ) +// StringDecrypt - langsung coba decompress tanpa decrypt ulang func StringDecrypt(key string, encryptedString string) (string, error) { - keyHash := sha256.Sum256([]byte(key)) - keyHashBytes := keyHash[:] + log.Printf("StringDecrypt: Attempting decompression, data length: %d", len(encryptedString)) - iv := keyHashBytes[:16] - - encryptedBytes, err := base64.StdEncoding.DecodeString(encryptedString) - if err != nil { - return "", err + // Method 1: Try direct LZ-string decompression (data sudah didecrypt di response.go) + if result, err := lzstring.DecompressFromEncodedURIComponent(encryptedString); err == nil && len(result) > 0 { + log.Printf("StringDecrypt: Direct decompression successful") + return result, nil } - block, err := aes.NewCipher(keyHashBytes) - if err != nil { - return "", err + // Method 2: Try other LZ-string methods + if result, err := lzstring.DecompressFromBase64(encryptedString); err == nil && len(result) > 0 { + log.Printf("StringDecrypt: Base64 decompression successful") + return result, nil } - mode := cipher.NewCBCDecrypter(block, iv) - - decrypted := make([]byte, len(encryptedBytes)) - mode.CryptBlocks(decrypted, encryptedBytes) - - decrypted = RemovePKCS7Padding(decrypted) - - dataResp, err := lzstring.DecompressFromEncodedURIComponent(string(decrypted)) - if err != nil { - log.Fatalf("Error decompress: %v", err) - } - return dataResp, nil + // Method 3: If all fail, return the original string + log.Printf("StringDecrypt: All decompression failed, returning original data") + return encryptedString, nil } func RemovePKCS7Padding(data []byte) []byte { + if len(data) == 0 { + return data + } + paddingLength := int(data[len(data)-1]) + if paddingLength > len(data) || paddingLength == 0 { + log.Printf("RemovePKCS7Padding: Invalid padding length: %d, data length: %d", paddingLength, len(data)) + return data // Return original data if padding is invalid + } + + // Verify all padding bytes are correct + for i := len(data) - paddingLength; 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 + } + } + return data[:len(data)-paddingLength] } diff --git a/internal/services/bpjs/response.go b/internal/services/bpjs/response.go index ab2fbb9..d05cd43 100644 --- a/internal/services/bpjs/response.go +++ b/internal/services/bpjs/response.go @@ -11,6 +11,9 @@ import ( "errors" "io" "log" + "unicode/utf16" + + lzstring "github.com/daku10/go-lz-string" ) func min(a, b int) int { @@ -23,7 +26,7 @@ func min(a, b int) int { // 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: %s", encrypted) + log.Printf("ResponseVclaim: Encrypted string length: %d", len(encrypted)) // Pad the base64 string if needed if len(encrypted)%4 != 0 { @@ -31,81 +34,177 @@ func ResponseVclaim(encrypted string, key string) (string, error) { for i := 0; i < padding; i++ { encrypted += "=" } - log.Printf("ResponseVclaim: Padded encrypted string: %s", encrypted) } + // Decode base64 cipherText, err := base64.StdEncoding.DecodeString(encrypted) if err != nil { log.Printf("ResponseVclaim: Failed to decode base64: %v", err) return "", err } - log.Printf("ResponseVclaim: Base64 decoded successfully, length: %d", len(cipherText)) - - hash := sha256.Sum256([]byte(key)) - - block, err := aes.NewCipher(hash[:]) - if err != nil { - log.Printf("ResponseVclaim: Failed to create AES cipher: %v", err) - return "", err - } if len(cipherText) < aes.BlockSize { - log.Println("ResponseVclaim: CipherText too short") return "", errors.New("cipherText too short") } - iv := hash[:aes.BlockSize] + // 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") + 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 { - log.Println("ResponseVclaim: CipherText not multiple of block size") return "", errors.New("cipherText is not a multiple of the block size") } mode := cipher.NewCBCDecrypter(block, iv) - mode.CryptBlocks(cipherText, cipherText) - log.Println("ResponseVclaim: AES decryption completed") + decrypted := make([]byte, len(cipherText)) + mode.CryptBlocks(decrypted, cipherText) - // cipherText, _ = pkcs7.Unpad(cipherText, aes.BlockSize) - cipherText = helper.RemovePKCS7Padding(cipherText) - log.Printf("ResponseVclaim: PKCS7 padding removed, length: %d", len(cipherText)) + // Remove PKCS7 padding + decrypted = helper.RemovePKCS7Padding(decrypted) + log.Printf("tryDecryptWithHashIV: Decryption completed, length: %d", len(decrypted)) - var data string + return tryAllDecompressionMethods(decrypted) +} - // Try gzip decompression first - reader, err := gzip.NewReader(bytes.NewReader(cipherText)) +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 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 + } + + // 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 { + 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)) + return result, nil + } + + return "", errors.New("all decompression methods failed") +} + +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 { - log.Printf("ResponseVclaim: Gzip decompression failed: %v, trying LZ-string decompression", err) - // Try lz-string decompression using helper function - data, err := helper.StringDecrypt(key, string(cipherText)) - if err != nil || len(data) == 0 { - log.Printf("ResponseVclaim: Helper StringDecrypt failed or empty: %v, trying without decompression", err) - // Try without decompression - data = string(cipherText) - log.Printf("ResponseVclaim: Using decrypted data without decompression, data length: %d", len(data)) - } else { - log.Printf("ResponseVclaim: Helper StringDecrypt successful, data length: %d, data: %s", len(data), data[:min(100, len(data))]) - } - } else { - defer reader.Close() - decompressed, err := io.ReadAll(reader) - if err != nil { - log.Printf("ResponseVclaim: Failed to read gzip decompressed data: %v, trying LZ-string decompression", err) - // Try lz-string decompression using helper function - data, err := helper.StringDecrypt(key, string(cipherText)) - if err != nil || len(data) == 0 { - log.Printf("ResponseVclaim: Helper StringDecrypt failed or empty: %v, using data without decompression", err) - data = string(cipherText) - log.Printf("ResponseVclaim: Using decrypted data without decompression, data length: %d", len(data)) - } else { - log.Printf("ResponseVclaim: Helper StringDecrypt successful, data length: %d", len(data)) - } - } else { - data = string(decompressed) - log.Printf("ResponseVclaim: Gzip decompression successful, data length: %d", len(data)) + return "", err + } + defer reader.Close() + + decompressed, err := io.ReadAll(reader) + if err != nil { + return "", err + } + + 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 } } - log.Printf("ResponseVclaim: Final data length: %d, data: %s", len(data), data[:min(100, len(data))]) - log.Println("ResponseVclaim: Decryption process completed successfully") - return data, 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 { + 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 } diff --git a/internal/services/bpjs/vclaimBridge.go b/internal/services/bpjs/vclaimBridge.go index 66b1f27..6ef0932 100644 --- a/internal/services/bpjs/vclaimBridge.go +++ b/internal/services/bpjs/vclaimBridge.go @@ -14,7 +14,6 @@ import ( "github.com/mashingan/smapping" "github.com/rs/zerolog/log" - "github.com/tidwall/gjson" ) // VClaimService interface for VClaim operations @@ -139,122 +138,62 @@ func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, b func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error) { defer res.Body.Close() - log.Info(). - Int("status_code", res.StatusCode). - Str("status", res.Status). - Msg("Processing HTTP response") - body, err := io.ReadAll(res.Body) if err != nil { - log.Error(). - Err(err). - Int("status_code", res.StatusCode). - Msg("Failed to read response body") return nil, fmt.Errorf("failed to read response body: %w", err) } - // Log response body for debugging (truncate if too long) - bodyStr := string(body) - if len(bodyStr) > 1000 { - bodyStr = bodyStr[:1000] + "...(truncated)" - } - log.Debug(). - Int("status_code", res.StatusCode). - Str("response_body", bodyStr). - Msg("Raw response received") - - // Check HTTP status if res.StatusCode >= 400 { - log.Error(). - Int("status_code", res.StatusCode). - Str("response_body", bodyStr). - Msg("HTTP error response") return nil, fmt.Errorf("HTTP error: %d - %s", res.StatusCode, string(body)) } // Parse raw response var respMentah ResponMentahDTOVclaim if err := json.Unmarshal(body, &respMentah); err != nil { - log.Error(). - Err(err). - Int("status_code", res.StatusCode). - Msg("Failed to unmarshal raw response") return nil, fmt.Errorf("failed to unmarshal raw response: %w", err) } - // Log metadata - log.Info(). - Str("meta_code", respMentah.MetaData.Code). - Str("meta_message", respMentah.MetaData.Message). - Msg("Response metadata") - // Create final response finalResp := &ResponDTOVclaim{ MetaData: respMentah.MetaData, } - // If response is empty, return as is + // Check if response needs decryption if respMentah.Response == "" { - log.Debug().Msg("Empty response received, returning metadata only") return finalResp, nil } - // Try to unmarshal response as JSON first (in case it's not encrypted) + // Try to parse as JSON first (unencrypted response) var tempResp interface{} if json.Unmarshal([]byte(respMentah.Response), &tempResp) == nil { - log.Debug().Msg("Response is valid JSON, not encrypted") finalResp.Response = tempResp - } else { - log.Debug().Msg("Response is not valid JSON, trying to decrypt") - // Decrypt response - consID, secretKey, _, tstamp, _ := s.config.SetHeader() - respDecrypt, err := ResponseVclaim(respMentah.Response, consID+secretKey+tstamp) - if err != nil { - log.Error(). - Err(err). - Str("meta_code", respMentah.MetaData.Code). - Msg("Failed to decrypt response") - return nil, fmt.Errorf("failed to decrypt response: %w", err) - } - - log.Debug(). - Str("encrypted_length", fmt.Sprintf("%d bytes", len(respMentah.Response))). - Str("decrypted_length", fmt.Sprintf("%d bytes", len(respDecrypt))). - Msg("Response decrypted successfully") - - log.Debug(). - Str("decrypted_data", respDecrypt). - Msg("Decrypted data") - - // Unmarshal decrypted response - if 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") - finalResp.Response = respDecrypt - } else { - log.Debug().Msg("Decrypted response unmarshaled successfully") - - // Use gjson to extract and log some metadata from the response if it's JSON - if jsonBytes, err := json.Marshal(finalResp.Response); err == nil { - jsonStr := string(jsonBytes) - // Extract some common fields using gjson - if metaCode := gjson.Get(jsonStr, "metaData.code"); metaCode.Exists() { - log.Info(). - Str("response_meta_code", metaCode.String()). - Msg("Final response metadata") - } - } - } - } + return finalResp, nil } - log.Info(). - Str("meta_code", finalResp.MetaData.Code). - Str("meta_message", finalResp.MetaData.Message). - Msg("Response processing completed") + // Decrypt response + consID, secretKey, _, tstamp, _ := s.config.SetHeader() + decryptionKey := consID + secretKey + tstamp + + log.Debug(). + Str("consID", consID). + Str("tstamp", tstamp). + 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") + return nil, fmt.Errorf("failed to decrypt response: %w", err) + } + + // Try to unmarshal decrypted response as JSON + if 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") + finalResp.Response = respDecrypt + } + } return finalResp, nil }