Skip to content

Instantly share code, notes, and snippets.

Created August 7, 2014 18:07
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save anonymous/81e62e87f173ec043c03 to your computer and use it in GitHub Desktop.
Save anonymous/81e62e87f173ec043c03 to your computer and use it in GitHub Desktop.
create two random-looking images that reveal a QR code when superposed
#!/bin/bash
# This script reads text from stdin, generates its QR code and splits it into two visual cryptographic images. Both images appear random and have as many whites as blacks. Only when print on transparent film (or extremely thin paper) and superposed, the ciphered qrcode appears as gray/black dots.
# We use the program "qrencode" to generate a QR at an average 7% error-correction rate ("low" quality; the lowest available). We create then a random array and we visual-XOR it with the QR code to retrieve the ciphered code. Only the random array (nonce or one-time pad) and the ciphered array are recorded, and only when both are superposed they reveal the QR code.
# The visual-cryptographic scheme used is inspired of http://leemon.com/crypto/VisualCrypto.html . It transforms 0s and 1s into diagonal 2x2 squares (01\10 and 10\01). The visual XOR either repeats (gray superposition; corresponding to a white dot) or flips (black superposition; corresponding to a black dot), following the QR code.
# Security wise, it should be nearly as hard to crack the QR code as the encoded text. The 7%-average error correcton does leak some information though (but in most practical conditions, an attacker already able to crack 93% of a code can almost surely crack it at 100%). Also, size and placemarks can approximately reveal the length of the text. Outside placemarks, QR codes approximately have as many whites as blacks, which are further unbiased through XORing.
# In practical terms, QR readers are able to read the graylike/black codes in most cases. Blurring or increasing distance can help reading it in case of difficulty.
FILE_PREFIX="visual_qrcode" # adapt it to your own whims
CRYPTOSEC_RAND_POOL="/dev/urandom" # a cryptographically-secure pool of randomness (preferably, a non-blocking one)
# Dependency test:
if ! AWK=$(which awk)
then
echo "error: $(basename $0) needs 'awk'"
exit 1
fi
if ! XXD=$(which xxd)
then
echo "error: $(basename $0) needs the 'xxd' hexdumper (usually packaged with 'vim')"
exit 1
fi
if ! QRENCODE=$(which qrencode)
then
echo "error: $(basename $0) needs command 'qrencode' to run (you may find it at package 'qrencode')"
exit 1
fi
# Early versions of qrencode (e.g., the one in ubuntu/precise) do not support ASCII output; we need version 3.3.0 or higher:
if [ $("$QRENCODE" -V 2>&1 | "$AWK" 'NR==1{split($3,VER,"."); printf("%03d%03d%03d\n", VER[1], VER[2], VER[3])}') -lt 003003000 ]
# this is a quick-and-dirty hack that works whenever version has three elements at most, each lesser than 999; the unportable alternative is to use "sort -V", and the complex-but-correct one is the one described at http://stackoverflow.com/a/4025065
then
echo "error: $(basename $0) requires qrencode version 3.3.0 or higher"
exit 1
fi
# a possible workaround (todo?) is to convert the PNG output to JPEG and then use "jp2a" to get it in ASCII form
QRCODE=$("$QRENCODE" -lL -8 -t ASCII | sed -e 's/##/1/g; s/ /0/g')
# qrencode reads from stdin; its ASCII mode outputs 2x1 dots with "#" as black and BLANK as white
SIZE=$(echo "$QRCODE" | wc -l)
for i in $(seq 1 $SIZE) # notice that it would not work with quotes (the list would be "one element" to iterate)
do
RNDCODE=$RNDCODE$("$XXD" -b -c 1 < "$CRYPTOSEC_RAND_POOL" | "$AWK" '{print $2}' ORS='' | head -c $SIZE)$'\n'
# xxd is an hexdumper that can output in 0s and 1s (option -b); it splits output by bytes in a single column (-c1)
# awk takes the second column (the one containing the bits) and sets the output record separator (ORS) to null to strip the linefeeds
# the pipe runs limitless; head closes it when enough 0s and 1s have gone through
# xxd could also limit it, option -l, but that indicates bytes not bits, so we still need head whenever 8 is not a factor of SIZE
done
# Notice a fundamental difference in the way that QRCODE and RNDCODE are generated. The output of "qrencode" ends with a linefeed and "sed" maintains this, but the final linefeed at the last line gets pruned because of the $() command substitution. Doing it with backticks or enclosed in quotes (unneeded since no variables) changes nothing.
# RNDCODE does have a trailing '\n' and echo outputs also one in the end by default, therefore QRCODE needs the standard "echo" while RNDCODE needs "echo -n".
BIG_QRCODE=($(echo "$QRCODE" | sed 's/0/00/g; s/1/11/g; p'))
# here, each dot is converted into a 2x2 square that repeat its value (0s and 1s duplicate then line is written again)
# running it in a subshell (parentheses) is a hack to split lines into array elements, making BIG_QRCODE actually an array
# notice that we cannot use quotes here; it works as long as elements have no spaces or tabs [1].
BIG_RNDCODE=($(echo -n "$RNDCODE" | sed -n 's/0/0X/g; s/1/X0/g; s/X/1/g; p; s/0/X/g; s/1/0/g; s/X/1/g; p'))
# each dot is converted into a 2x2 square with a diagonal pattern: 0 into 01\10 and 1 into 10\01
# we disable auto printing (option -n) switch 0 to 01 (through temp symbol X) and 1 to 10, print line, switch 0s and 1s, print line
ITER="$(seq 0 $((2*SIZE-1)))"
for i in $ITER
do
for j in $ITER
do
CIPHERCODE[i]=${CIPHERCODE[i]}$(( (${BIG_QRCODE[i]:j:1} + ${BIG_RNDCODE[i]:j:1})%2 ))
done
done
((SIZE<<=1)) # a bitshift multiplication by 2
printf '%s\n%s\n%s\n' P1 "$SIZE $SIZE" "${BIG_RNDCODE[@]}" > "$FILE_PREFIX-$$-otp.pbm"
printf '%s\n%s\n%s\n' P1 "$SIZE $SIZE" "${CIPHERCODE[@]}" > "$FILE_PREFIX-$$-cipher.pbm"
# alternative: IFS=$'\n'; echo -e "P1\n$SIZE $SIZE\n${CIPHERCODE[*]}"
# [1]: What happens when the array contains unescaped spaces or tabs? We can still use the same trick by setting IFS to linefeeds only. IFS stands for "internal field separator" and it defines which characters are to be considered as field separators for arrays (e.g., the "$i" elements in "$@" when parsing input parameters or, more generally, the elements looping in a "for ... in ..." loop).
# By default, $IFS is space tab linefeed. If our lines contain spaces or tabs, we can set IFS=$'\n' (outside the subshell!) so that the "echo in a subshell" trick splits at the linefeeds only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment