如何在Kotlin中实现与Ruby遗留代码一致的HMAC签名?
Got it, let's break down what's happening here and fix your Kotlin code to match the Ruby behavior exactly.
First, understand what the Ruby code is doing with bin_key = Array(decoded_key).pack('H*')
That line is a bit tricky, so let's unpack it step by step:
decoded_keyis the ASCII string you get after Base64-decoding the input ("any carnal pleasure."in your example).Array(decoded_key)converts the string into an array of individual characters (e.g.,['a', 'n', 'y', ' ', ...]), which is redundant here—joining it back gives the original string.pack('H*')treats the input string as a hexadecimal source:- It filters out any characters that aren't valid hex digits (0-9, a-f, A-F).
- If the remaining string has an odd length, it appends a
0to make it even (since hex digits are paired to form bytes). - It converts the cleaned hex string into a binary byte array.
Fixing the Kotlin code
We need to replicate that exact hex-to-binary conversion logic for the key. Here's how to do it:
Step 1: Add a helper function to mimic Ruby's pack('H*')
This function will clean the input string, handle odd lengths, and convert it to a byte array:
fun mimicRubyPackHstar(input: String): ByteArray { // Filter out non-hex characters (matches Ruby's behavior) val cleanedHex = input.replace(Regex("[^0-9a-fA-F]"), "") // Add a trailing 0 if length is odd (Ruby does this automatically) val evenLengthHex = if (cleanedHex.length % 2 != 0) cleanedHex + "0" else cleanedHex // Convert hex string to byte array return ByteArray(evenLengthHex.length / 2) { index -> val start = index * 2 val highNibble = evenLengthHex[start].digitToInt(16) shl 4 val lowNibble = evenLengthHex[start + 1].digitToInt(16) (highNibble or lowNibble).toByte() } }
Step 2: Update your main Kotlin logic
Use the helper function to process the key exactly like Ruby does, then compute the HMAC:
import java.nio.charset.Charsets import java.security.MessageDigest import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import java.util.Base64 fun main() { val base64Key = "YW55IGNhcm5hbCBwbGVhc3VyZS4=" // Decode Base64 to bytes, then convert to ASCII string (matches Ruby's string encoding) val decodedKeyBytes = Base64.getDecoder().decode(base64Key) val decodedKeyStr = String(decodedKeyBytes, Charsets.US_ASCII) // Process the key to match Ruby's Array(decoded_key).pack('H*') val binKey = mimicRubyPackHstar(decodedKeyStr) val toSign = "some string to sign" val hMacSHA256 = Mac.getInstance("HmacSHA256") val secretKey = SecretKeySpec(binKey, "HmacSHA256") hMacSHA256.init(secretKey) // Compute HMAC (use UTF-8 for string-to-byte conversion, matches Ruby's default) val hmacBytes = hMacSHA256.doFinal(toSign.toByteArray(Charsets.UTF_8)) // Convert to lowercase hex string (matches Ruby's hexdigest output) val hmacHex = hmacBytes.joinToString("") { "%02x".format(it) } println(hmacHex) }
Key notes about the alignment with Ruby:
- String encoding: We use
Charsets.US_ASCIIto convert the Base64-decoded bytes to a string, which matches Ruby's default encoding for this ASCII-only input. - Hex cleaning: The regex filters out non-hex characters exactly as Ruby's
pack('H*')does. - Odd length handling: We append a
0for odd-length cleaned hex strings to match Ruby's behavior. - Output format: We generate a lowercase hex string, which matches Ruby's
hexdigestresult.
Running this Kotlin code will produce the exact same HMAC output as your original Ruby code.
内容的提问来源于stack exchange,提问作者freethinker




