Search code examples
javascriptcryptographycryptojs

How to pass a binary string to `btoa` to encode the raw value


I need to modify this (working) code and replace CryptoJS functions with native JS.

The variable b64encoded has the correct value, but my attempt with native code (encodedIncorrect) is not. Can you help feed btoa the correct input?

Working, but 3rd party libs cannot be used on the platform I'm developing for:

const encrypted = CryptoJS.HmacSHA1("message", "secret");
// encrypted = "0caf649feee4953d87bf903ac1176c45e028df16" << Hexadecimal encoded string
const b64encoded = CryptoJS.enc.Base64.stringify(encrypted);
// b64encoded = "DK9kn+7klT2Hv5A6wRdsReAo3xY="

Not working, specifically the encoding of a binary string by btoa:

const encrypted_vendor = VENDOR.hmacSHA1("message", "secret") // Vendor-provided HMAC-SHA1 func
// encrypted_vendor = "0caf649feee4953d87bf903ac1176c45e028df16"

// Split hex string into two-character byte representations for binary conversion
function splitHex(hexString) {
    let chunks = [];
    for (var i = 0, charsLength = hexString.length; i < charsLength; i += 2) {
        chunks.push(hexString.substring(i, i + 2));
    }
}
// Convert hex characters to binary value
function hexCharToBinary(value) {
    return (parseInt(value, 16).toString(2)).padStart(8, '0');
}

binaryChunks = splitHex(encrypted)
var binaryStr = ""
binaryChunks.forEach(i => binaryStr += hexCharToBinary(i))
// binaryStr = "0000110010101111011001001001111111101110111001001001010100111101100001111011111110010000001110101100000100010111011011000100010111100000001010001101111100010110"
encodedIncorrect = btoa(binaryStr)
// encodedIncorrect = "MDAwMDExMDAxMDEwMTExMTAxMTAwMTAwMTAwMTExMTExMTEwMTExMDExMTAwMTAwMTAwMTAxMDEwMDExMTEwMTEwMDAwMTExMTAxMTExMTExMDAxMDAwMDAwMTExMDEwMTEwMDAwMDEwMDAxMDExMTAxMTAxMTAwMDEwMDAxMDExMTEwMDAwMDAwMTAxMDAwMTEwMTExMTEwMDAxMDExMA=="

I've also verified that the binary output is correct using this site (https://cryptii.com/pipes/binary-to-base64), which encodes it to the correct base64 when set to binary input.


Solution

  • You are encoding the string representation of the binary digits, not the actual data.

    You need a binary string, which you can construct e.g. using String.fromCharCode:

    const hexString = "0caf649feee4953d87bf903ac1176c45e028df16"
    
    // split into two character groups (one byte each), see e.g.
    // https://stackoverflow.com/questions/6259515/how-can-i-split-a-string-into-segments-of-n-characters
    // using /.{1,2}/g may be preferrable for potential error handling
    // but if there is always an even amount of characters, it doesn't matter.
    let hexCharacterPairs = hexString.match(/../g);
    
    // parse two hex characters as number
    let bytes = hexCharacterPairs.map(e => parseInt(e, 16));
    
    // generate symbols from the bytes
    let binaryStringSymbols = bytes.map(e => String.fromCharCode(e));
    
    // get a string from all the symbols
    let binaryString = binaryStringSymbols.join("");
    
    // use the binary string to get a base64 encode
    let base64 = btoa(binaryString);
    
    console.log(base64);
    
    // or in one line
    console.log(btoa("0caf649feee4953d87bf903ac1176c45e028df16".match(/../g).map(e => String.fromCharCode(parseInt(e, 16))).join("")));

    Note, that the above snippet has no error handling, e.g. for odd numbers of characters in the input, invalid characters, etc.