Search code examples
c#cryptographyaesqr-codecryptojs

Encrypt QRCode payload with input length the same as the output length


Im trying to encrypt the data payload of a dynamic QRCode.

We have an app that generates a QRCode from dynamic data and we would like to have an option to encrypt the payload if so desired.

Because the size limitations inherent in QRCodes, one of the requirements is that the payload data must be the same size as the encrypted data. If the encrypted data is different/too large, the QRCode may fail to scan/decode correctly.

I have tried to use AES encryption in CTR mode (stream as opposed to block) as suggested elsewhere. The result is that the output cipher bytes are same size as the input bytes, however converting the bytes to a char friendly format (eg base64) produces a string length much longer than that of the original payload.

Is there way to encrypt a string so that the plain text and encrypted string length are the same?

According to this MIT pdf located here, https://courses.csail.mit.edu/6.857/2014/files/12-peng-sanabria-wu-zhu-qr-codes.pdf, they mention already existing solutions which they call SEQR (Symmetric Encrypted QR) codes. However I am yet to find a solution which, as mentioned in the article, produces an encrypted output that is the same length is the plain text input.

We are using C# .NET in our backend, the ZXing QRCode library and Javascript on our front end, with CryptoJS.

I have also created a JSFiddle demostrating AES in CTR mode.

http://jsfiddle.net/s1jeweja/5/

var options = { mode: CryptoJS.mode.CTR, padding: CryptoJS.pad.NoPadding };  

/*** encrypt */  
var json = CryptoJS.AES.encrypt("some plain text", "secret", options);  
var ciphertext = json.ciphertext.toString(CryptoJS.enc.UTF8);  
document.getElementById("id2").innerHTML = ciphertext;
//debugger;
/*** decrypt */  
var decrypted = CryptoJS.AES.decrypt(json, "secret", options);  
var plaintext = decrypted.toString(CryptoJS.enc.Utf8);  



document.getElementById("decrypt").innerHTML = plaintext; // text can be a random lenght

Solution

  • You are looking for a Format Preserving Encryption which converts plaintext text characters to cyphertext text characters. One way to do that is an adaptation of the classic Vigenère cipher. Generate a keystream of pseudo-random numbers in the range 0..25 and use them to do a cyclic shift of the plaintext characters round the alphabet.

    You are already using AES-CTR, so use that to generate your keystream. Mask off five bits of the keystream. If those five bits are in 0..25 then use them to do a cyclic shift of the next plaintext character to the corresponding cyphertext character. If the five bits are in the range 26..31 then discard them and get another five bits.

    In pseudocode, this looks something like:

    textToTextEncode(plaintext)
      for each character p in plaintext
        c <- p + getNextShift()
        if c > 'z'
          c <- c - 26
        end if
        write(c, cyphertextFile)
      end for
    end textToTextEncode()
    
    getNextShift()
      repeat
        b <- getNextByte(AES-CTR)
        b <- b & 0b00011111  // Binary mask.
      until b < 26
      return b
    end getNextShift()
    

    For decryption you will need to subtract the shift value and add 26 if the result falls below 'a'.

    ETA: As you can see from the comments below, you will need a nonce (AKA IV) that is unique for each QRCode. Set aside a fixed number of bytes at the start or end of each QRCode for the nonce. Do not encrypt those bytes, you have to leave them in the clear. Extract them from the full QRCode and use them to initialise your AES-CTR method. Then use that AES-CTR to decrypt the rest of that QRCode. The more bytes, up to 16, the more security you will have.