2

Hi im trying to read my greenpass certificate, and i know that when you scan the QR code it will appear something like this HC1:NHFDFGDF......, i also know this is encoded as base45 so i made a little javascript decoder, this is my code:

const base45 = require('base45');

//of course my personal HC1: isnt here
const encodedData = 'C0C9BQF2LVU.TMBX4KDL*XD/GPWBILC9GGBYPLR-SAG1CSQ6U7SSQY%SJWLK34JWLG56H0API0TUL:12>'

const decodedData = base45.decode(encodedData).toString('utf-8');

console.log(decodedData);

And this is the response:

xڻ�⻈Q��C#?��-E�����K�BX���ֳI%|�zl�ȼ�qIbY㪤��
�Ң<�Ҳ�L�� ?��0gO+C��+�`��`׀0�H'W�P���WweϤ�|��I)yLI)%YFF��f�FfI�ť��y��%�We�+�$*'ޕ�������kh�f��I�9�����F����F�I�)LI%�&�ƖfIeY��������)�IY���F {
                                                                                                                                            �sVp����*8���z�8*��:;'������
     pMN�+*��*�+JN�+��  �
                         ��**K-J5�3�3�p8���yM]�o����E�_��O.�ϩ����
�g�˽��\�����="��]۷
����������{��Kq ��

what i read is that its decoding of base45 will lead to zlib compressed file, where the decompression will lead a CBOR web token, but im stuck, can you help me ? Is this result normal ? also im still learning

Sven Eberth
  • 3,057
  • 12
  • 24
  • 29
Sh4dow
  • 21
  • 1
  • 1
  • 4
  • That is not valid Base45 input. Please recheck that you have correctly copied and pasted it into the code. Your decoding does in fact start with two bytes indicating a zlib header. However you should never post binary data in printed form, since it cannot be read. All those white-on-black question marks are parts of the binary that could not be printed, and so are lost and completely useless to someone trying to help you. Always put binary data in a question as hexadecimal or Base-64. – Mark Adler Aug 01 '21 at 23:49
  • this is part of the string that follows HC1:, when we scan the QR code: NCFOXN%TSMAHN-H.L8%38Q%T6$823S0IIYMJ the one that i read that is encoded as base45, here is where i've seen it, but i wanna make it a little different: https://dev.to/lmillucci/javascript-how-to-decode-the-greenpass-qr-code-3dh0, and i changed the utf-8 to hex, this was what i got, its just a part of it, not all: 78dabbd4e2bb88518dc543233ff5ec2d45cfde05918c9a4b1893024258a4128ff2d6b349257ca87a6cc9c8bc9071496259e3aaa4e4cc0ac3d2a23cabd2b2e44 – Sh4dow Aug 02 '21 at 00:57
  • **In the question.** Put the Base64 in the question. Not in these comments. (As you noticed, comments are length limited. And they are not where information needed to answer the question belong. That place is called the question.) And please delete the attempted printout of the binary in the question. – Mark Adler Aug 02 '21 at 01:17
  • That is a valid zlib stream. So yes, your result normal and correct. – Mark Adler Aug 02 '21 at 01:19
  • thanks, in the web site i showed he was using pako to uncompress it, but is giving me an error, is it possible to use zlib to uncompress it ? – Sh4dow Aug 02 '21 at 01:32
  • Yes, turns out that zlib can decompress a zlib stream. – Mark Adler Aug 02 '21 at 03:38
  • I solved the issue and updated my answer with a fully functional source. – jumpjack Sep 11 '21 at 19:32

2 Answers2

5

I solved the problem using only browser resources (no node.js); I also found some official sources for explanations, test data, field decoding (see at the end of the answer):

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="generator" content="PSPad editor, www.pspad.com">
  <title></title>
  </head>
  <body>
  <textarea id="encoded" name="encoded" cols=100 rows=10></textarea><br>
  <textarea id="decoded" name="decoded" cols=100 rows=10 value = "prova">hello</textarea><br>
  <span id="plain1" name="plain1">-</span> --&gt;
  <span id ="cod" name="cod">-</span> --&gt;
  <span id="plain2" name="plain2">-</span><br>
  <button onclick="test()">Test</button><br>
  <button onclick="vai()">Vai</button><br>
  <script src="pako.min.js"></script>
  <script src="my_base45_2.js"></script>
  <script src="cbor.js" type="text/javascript"></script>

<script>

// https://dev.to/lmillucci/javascript-how-to-decode-the-greenpass-qr-code-3dh0

function convert(decoded) {
    text="";
    for (var i = 0; i < decoded.length; i++) {
        text +=  String.fromCharCode(decoded[i]);
    }
    console.log("Text=",text);
    values = text.split(",");
    console.log("values: " ,values);
    final = "";
    for (var i=0; i<values.length; i++) {
        final += String.fromCharCode(values[i])
    }
    return final;
}

function test() {

    source = document.getElementById("decoded").value;
    console.log("source=",source);
    document.getElementById("plain1").innerHTML = source;

    var enc = new TextEncoder();
    buff= enc.encode(source);
    console.log("buff:" , buff);

    encoded2 = encode(buff);
    console.log("encoded2:" , encoded2);
    document.getElementById("cod").innerHTML = encoded2;

    source = encoded2;
    console.log("Encoded source=",source);
    decoded = decode(source).enc;
    converted = convert(decoded);

    console.log("Decoded=", decoded);
    console.log("converted=", converted);

    document.getElementById("plain2").innerHTML = converted;
}


function buf2hex(buffer) {
// https://stackoverflow.com/questions/34309988/byte-array-to-hex-string-conversion-in-javascript
    var u = new Uint8Array(buffer),
        a = new Array(u.length),
        i = u.length;
    while (i--) // map to hex
        a[i] = (u[i] < 16 ? '0' : '') + u[i].toString(16);
    u = null; // free memory
    return a.join('');
};


function typedArrayToBuffer(array) {
// https://stackoverflow.com/questions/37228285/uint8array-to-arraybuffer
    return array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset)
}


function vai() {
    source = document.getElementById("encoded").value;
    console.log("Encoded source=",source);

    // Decode BASE45:
    decoded = decode(source).enc;

    // Unzip the decoded:
    COSEbin =  pako.inflate(decode(source).raw);
        COSE = buf2hex(COSEbin);

        var typedArray = new Uint8Array(COSE.match(/[\da-f]{2}/gi).map(function (h) {
          return parseInt(h, 16)
        })) // https://stackoverflow.com/questions/43131242/how-to-convert-a-hexadecimal-string-of-data-to-an-arraybuffer-in-javascript
        var unzipped = typedArray.buffer;

        [headers1, headers2, cbor_data, signature] = CBOR.decode(unzipped);
        cbor_dataArr = typedArrayToBuffer(cbor_data);
        greenpassData  = CBOR.decode(cbor_dataArr);

        console.log(greenpassData);
}
</script>
  </body>
</html>

The key is the "double CBOR decoding": once is not enough:

CBOR decoding:

[headers1, headers2, cbor_data, signature] = CBOR.decode(unzipped);

Conversion to Array:

cbor_dataArr = typedArrayToBuffer(cbor_data);

Further CBOR conversion:

greenpassData  = CBOR.decode(cbor_dataArr);

My version of base45 decoder ("my_base45_2.js"), adapted from a node.js version:

 function encode(uint8array) {
    var output = [];

    for (var i = 0, length = uint8array.length; i < length; i+=2) {
      if (uint8array.length -i > 1) {
         var x = (uint8array[i]<<8)+ uint8array[i+1]
         var [ e, x ]  = divmod(x, 45*45)
         var [ d, c ] = divmod(x, 45)
         output.push(fromCharCode(c) + fromCharCode(d) + fromCharCode(e))
     } else {
         var x = uint8array[i]
         var [ d, c ] = divmod(x, 45)
         output.push(fromCharCode(c) + fromCharCode(d))
     }
    }
    return output.join('')
  };

  var divmod = function divmod(a,b) {
    var remainder = a
    var quotient = 0
    if (a >= b) {
        remainder = a % b
    quotient = (a - remainder) / b
    }
    return [ quotient, remainder ]
  }

  const BASE45_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
  var fromCharCode = function fromCharCode(c) {
    return BASE45_CHARSET.charAt(c);
  };

function decode(str) {
    var output = []
    var buf = []

    for(var i = 0, length=str.length; i < length; i++) {
//console.log(i);    
       var j = BASE45_CHARSET.indexOf(str[i])
       if (j < 0)
                console.log('Base45 decode: unknown character n.', i, j);
              //throw new Error('Base45 decode: unknown character');
       buf.push(j)
    }

    for(var i = 0, length=buf.length; i < length; i+=3) {
       var x = buf[i] + buf[i + 1] * 45
       if (length - i >= 3) {
          var [d, c] = divmod(x + buf[i + 2] * 45 * 45,256)
          output.push(d)
          output.push(c)
       } else {
         output.push(x)
       }
    }
console.log("output",output);    
    var enc = new TextEncoder();
    return {"enc" : enc.encode(output), "raw" : output};
    //return Buffer.from(output);
  };

These are the decoding steps:

QR code --> QR DECODER --> RAW QR-decoded string --> BASE45 decoder --> zlib compressed string --> pako library --> COSE string --> CBOR decoder --> CBOR string --> CBOR decoder --> final JSON file

My reference guides for decoding greenpass:

jumpjack
  • 841
  • 1
  • 11
  • 17
0
const base45 = require('base45');
const encodedData = 'NCFOXN%TSMAHN-H.L8%38Q%T6$823S0IIYMJ 43%C0C9BQF2LVU.TMBX4KDL*XD/GPWBILC9GGBYPLR-SAG1CSQ6U7SSQY%SJWLK34JWLG56H0API0TUL:12'
const decodedData = base45.decode(encodedData).toString('hex');
console.log(decodedData);

Result:

78dabbd4e2bb88518dc543233ff5ec2d45cfde05918c9a4b1893024258a4128ff2d6b349257ca87a6cc9c8bc9071496259e3aaa4e4cc0ac3d2a23cabd2b2e44cabd0203fabd030674f2b0343ab80102bdf60aba060d78030e34813275713f750b7d0c8c808577765cfa4e47ca0894929794c4929255946064686ba0666ba46664999c5a5be997999c52587571665e62ba4242a04271ede95929a949b98eb1fe4ae6b6800048666969649b90539aea1fa86fa4686fa86a6469649c5294c4925e999162606a6c6960606664965055986868696c606a60606a6c929f949598696a646207b0ccd92f312739724a5e515b8b82a38faf8ba7aba382af8843a3b0627a5e7e5f8841eee0d5670f3f4f10c704d4ecb2b012ab2812ab2012b4a4ecf2bc9f609f50cb6812a2a4b2d4a35d433d033887038a4b4a4794d5dc66f8fcdf9b545815fb9a64f2ebfcfa9f9f2f7c71b458d67a6cbbdb38d0f5cf1f1c7d9e7013d22d1de1c5ddbb70ac9fd88b9cff2ecdf14c6e77bbe854b0100710988a7
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Sh4dow
  • 21
  • 1
  • 1
  • 4
  • The second thing cannot be the result of the first thing. The `encodedData` in the first part is 120 characters, which decodes to 80 bytes. The "Result" is 758 hexadecimal characters which decodes to 379 bytes. – Mark Adler Aug 02 '21 at 03:37
  • The Result is a valid and complete zlib stream. – Mark Adler Aug 02 '21 at 03:37