├── .gitignore ├── README.md ├── docs └── output-example.png ├── examples ├── cat │ ├── cat.jpg │ └── qrcodes-cat.jpg.pdf ├── divina-commedia │ ├── README.md │ ├── difference-screenshot.png │ ├── divina-commedia.txt │ ├── qrcodes-divina-commedia.txt.pdf │ └── qrcodes-h-level-divina-commedia.txt.pdf └── moby-dick │ ├── README.md │ ├── difference-screenshot.png │ ├── moby-dick.txt │ ├── qrcodes-h-moby-dick.txt.pdf │ └── qrcodes-moby-dick.txt.pdf └── papiro.sh /.gitignore: -------------------------------------------------------------------------------- 1 | debug/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Papiro 2 | 3 | **Papiro encodes and decodes files to/from QR Code(s).** 4 | 5 | You can encode a single file or a full directory, in the latter case the data are zipped before the encoding. 6 | 7 | The QR Codes are saved in a single (eventually multi page) pdf, ready to print; the rebuild process is done on a group of QR Codes' photos. Papiro is useful to save files in a non digital fashion for backup and portability pourposes using a printer. 8 | You can encode passwords, secrets, credentials, 2FA backup codes, crypto seeds ([mind the privacy](#privacy--encryption)!) and any text/binary file. 9 | 10 | _Papiro_ is the italian word for "papyrus" :page_with_curl: 11 | 12 | ## Why QR Codes on paper? 13 | 14 | Because paper seems the [most resilient and cost-effective storage medium on earth](https://superuser.com/questions/374609/what-medium-should-be-used-for-long-term-high-volume-data-storage-archival), at least for low volume data, and QR Codes have an interesting error-checking system. So it is a nice backup solution to pair with digital ones. 15 | 16 | ## Specs & Performances 17 | 18 | Papiro uses the QR Code v40 (177x177 matrix) with low (default, you can change it with the -l option) error correction level; [according to the specs](https://www.qrcode.com/en/about/version.html) with this config we can save 2953 bytes in binary mode on a single QR Code. An A4 sheet can accomodate 12 QR Codes for *an unbelievable total of ~34.60KB!* So a standard 500 sheets pack easily stores ~16.89MB, or ~33.79MB with an double side print. 19 | With the best (high) error correction level you can print ~14.91KB/sheet. 20 | 21 | ## Requirements 22 | 23 | Papiro is a simple shell script and needs these binaries: 24 | 25 | - `convert` 26 | - `qrencode` 27 | - `zbarimg` 28 | - `montage` 29 | 30 | ### Linux 31 | 32 | Install the required binaries using your favorite package manager, e.g.: 33 | 34 | ``` 35 | apt-get install imagemagick 36 | apt-get install qrencode 37 | apt-get install zbar-tools # Can have a different name, eg. "zbar" 38 | ``` 39 | If you get an error on Linux about the *"convert: attempt to perform an operation not allowed by the security policy "* add the following to */etc/ImageMagick-7/policy.xml*: 40 | 41 | ``` 42 | 43 | 44 | ``` 45 | 46 | ### Mac 47 | 48 | Install the required binaries using [brew](https://brew.sh): 49 | 50 | ``` 51 | brew install imagemagick 52 | brew install qrencode 53 | brew install zbar 54 | ``` 55 | 56 | ### Windows 57 | 58 | I don't own a Windows PC, sorry; so any contributions are welcome to test Papiro and write a brief startup guide. Fork the project and create a pull request, thank you! 59 | 60 | ## Installation 61 | 62 | Clone the repo and make the script executable: 63 | 64 | ``` 65 | git clone https://github.com/dtonon/papiro.git 66 | cd papiro 67 | chmod +x papiro.sh 68 | ./papiro.sh -h 69 | ``` 70 | You can even [download/copy](https://raw.githubusercontent.com/dtonon/papiro/master/papiro.sh) the single `papiro.sh` file and make it executable, that's all! 71 | 72 | ## Usage 73 | 74 | ``` 75 | # Encode a file to QR Codes in single pdf 76 | ./papiro.sh -c memo.txt 77 | 78 | # Zip and encode a file 79 | ./papiro.sh -c divina-commedia.txt -z 80 | 81 | # Encode a file with the best error correction 82 | ./papiro.sh -c important-data.xml -lH 83 | 84 | # Encode a directory to qrcodes (using a zip file) 85 | ./papiro.sh -c mydata/ 86 | 87 | # Interactively create a new vim encrypted file and then process it 88 | ./papiro.sh -x 89 | 90 | # Encode a file to QR Codes keeping the filename secret 91 | ./papiro.sh -c cat.jpg -a 92 | 93 | # Decode a group of images to rebuild a file 94 | # Note: files have to be correctly ordered by name, 95 | # cameras create sequential pics with a correct filename 96 | ./papiro.sh -r photos/ -o data.json 97 | ``` 98 | Papiro options: 99 | 100 | ``` 101 | -z Zip the file(s) 102 | -l Set the QR Code error correction level (L|M|Q|H); default is L(ow) 103 | -o Set the output filename 104 | -a Anonymous mode, don't annotate the original filename 105 | -s Create a papiro of the script itself, useful for archiving along with the encoded data 106 | -h Show the help 107 | -d Debug mode, create a debug/ dir with the temp images 108 | ``` 109 | 110 | ## Example 111 | 112 | In the examples/ dir you can find a [nice 60KB cat's photo](examples/cat/cat.jpg) and the [2 pages QR Codes-pdf-papiro](examples/cat/qrcodes-cat.jpg.pdf) generated with the following command: 113 | 114 | ``` 115 | ./papiro.sh -c cat.jpg 116 | ``` 117 | This is a preview of the generated pdf (top cropped A4 page): 118 | 119 | [![Output pdf example](docs/output-example.png)](examples/cat/qrcodes-cat.jpg.pdf) 120 | 121 | Or you can enjoy the full [Divina Commedia in 7 pages](examples/divina-commedia/README.md) or [Moby-Dick in 15 pages](examples/moby-dick/README.md), too! 122 | 123 | ## Privacy & Encryption 124 | 125 | Remember that a QR Code obfuscates your data but it does not protect them in any way; if your data need privacy you *have* to encrypt them before using Papiro. This is also useful to avoid the [printer's attack surface](https://krebsonsecurity.com/2021/07/microsoft-issues-emergency-patch-for-windows-flaw/). To apply a good privacy layer you can use [GPG](https://gnupg.org/), a password protected zip (also useful to reduce the file size and so the number of QR Codes) or the built-in "new vim encrypted file": 126 | 127 | ``` 128 | ./papiro.sh -x 129 | ``` 130 | 131 | You can also use the `-a` (*Anonymous*) option to don't annotate the filename on the created pdf. 132 | 133 | ## Self mode 134 | You can create a papiro of papiro.sh itself using the -s flag: 135 | 136 | ``` 137 | ./papiro -s 138 | ``` 139 | This command encode the script in QR codes with a brief description about how to decode and use it; it is useful to include with the encoded data, to ensure a easy way to decode it even if your don't have the original Papiro script and are using an air gapped system. -------------------------------------------------------------------------------- /docs/output-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtonon/papiro/8e95cd3c5255de17cb87d8bb6a07545d76d984ec/docs/output-example.png -------------------------------------------------------------------------------- /examples/cat/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtonon/papiro/8e95cd3c5255de17cb87d8bb6a07545d76d984ec/examples/cat/cat.jpg -------------------------------------------------------------------------------- /examples/cat/qrcodes-cat.jpg.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtonon/papiro/8e95cd3c5255de17cb87d8bb6a07545d76d984ec/examples/cat/qrcodes-cat.jpg.pdf -------------------------------------------------------------------------------- /examples/divina-commedia/README.md: -------------------------------------------------------------------------------- 1 | # Print the Divina Commedia in just 7 pages 2 | 3 | With [Papiro](https://github.com/dtonon/papiro) you can print the full [Divina Commedia](divina-commedia.txt) by Dante Alighieri (19.567 lines for 557KB of plain text) in [just 7 pages](qrcodes-divina-commedia.txt.pdf). It is easy: 4 | 5 | ``` 6 | ./papiro.sh -z -c divina-commedia.txt 7 | ``` 8 | 9 | About 44 pages would be required to print it with a 6pt font size (average minimum readeable size, while for an good OCR should be at least 9pt) in a multi column document, without taking advantage of any error correction. Spot the differences: 10 | 11 | ![Multi columns 6pt print vs QR Codes](difference-screenshot.png) 12 | 13 | Ok, you can't read it at a glance, but has a nice nerd factor :) 14 | 15 | If you are a real paranoid archivist feel free to increase the error correction level, but you have to invest [even 15 pages](qrcodes-h-level-divina-commedia.txt.pdf): 16 | 17 | ``` 18 | ./papiro.sh -z -c divina-commedia.txt -lH 19 | ``` 20 | Expensive, but suitable for a military-grade disaster recovery plan for italian literature. -------------------------------------------------------------------------------- /examples/divina-commedia/difference-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtonon/papiro/8e95cd3c5255de17cb87d8bb6a07545d76d984ec/examples/divina-commedia/difference-screenshot.png -------------------------------------------------------------------------------- /examples/divina-commedia/qrcodes-divina-commedia.txt.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtonon/papiro/8e95cd3c5255de17cb87d8bb6a07545d76d984ec/examples/divina-commedia/qrcodes-divina-commedia.txt.pdf -------------------------------------------------------------------------------- /examples/divina-commedia/qrcodes-h-level-divina-commedia.txt.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtonon/papiro/8e95cd3c5255de17cb87d8bb6a07545d76d984ec/examples/divina-commedia/qrcodes-h-level-divina-commedia.txt.pdf -------------------------------------------------------------------------------- /examples/moby-dick/README.md: -------------------------------------------------------------------------------- 1 | # Print Moby-Dick in just 15 pages 2 | 3 | With [Papiro](https://github.com/dtonon/papiro) you can print the full [Moby-Dick](moby-dick.txt) by Herman Melville (22.314 lines / 1,3MB plain text) in [just 15 pages](qrcodes-moby-dick.txt.pdf). It is easy: 4 | 5 | ``` 6 | ./papiro.sh -c moby-dick.txt -z 7 | ``` 8 | 9 | About 101 pages would be required to print it with a 6pt font size (average minimum readeable size, while for an good OCR should be at least 9pt) in a multi column document, without taking advantage of any error correction. Spot the differences: 10 | 11 | ![Multi columns 6pt print vs QR Codes](difference-screenshot.png) 12 | 13 | Ok, you can't read it at a glance, but has a nice nerd factor :) 14 | 15 | If you are a real paranoid archivist feel free to increase the error correction level, but you have to invest [even 34 pages](qrcodes-h-moby-dick.txt.pdf): 16 | 17 | ``` 18 | ./papiro.sh -c moby-dick.txt -z -lH 19 | ``` 20 | Expensive, but suitable for a military-grade disaster recovery plan for the Great American Novel. -------------------------------------------------------------------------------- /examples/moby-dick/difference-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtonon/papiro/8e95cd3c5255de17cb87d8bb6a07545d76d984ec/examples/moby-dick/difference-screenshot.png -------------------------------------------------------------------------------- /examples/moby-dick/qrcodes-h-moby-dick.txt.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtonon/papiro/8e95cd3c5255de17cb87d8bb6a07545d76d984ec/examples/moby-dick/qrcodes-h-moby-dick.txt.pdf -------------------------------------------------------------------------------- /examples/moby-dick/qrcodes-moby-dick.txt.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtonon/papiro/8e95cd3c5255de17cb87d8bb6a07545d76d984ec/examples/moby-dick/qrcodes-moby-dick.txt.pdf -------------------------------------------------------------------------------- /papiro.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Project: https://github.com/dtonon/papiro 4 | 5 | # Idea from https://www.grant-trebbin.com/2015/05/encode-and-decode-file-backed-up-as.html 6 | 7 | function show_help { 8 | echo -e "Papiro encodes and decodes file(s) to/from QR Codes, to print and store them phisically." 9 | echo -e "You can encode a single file or a full directory, in this latter case the data are zipped before the encoding." 10 | echo -e "The qrcodes are saved in a single pdf, ready to print; the rebuild process is done on a group of acquired photos." 11 | echo -e "\nUsage" 12 | echo -e "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"; 13 | echo -e "\033[1mEncode a file in qrcodes\033[0m\n./papiro.sh -c file|directory [-za] [-l L|M|Q|H] [-o file.pdf]\n" 14 | echo -e "\033[1mRebuild file from photos\033[0m\n./papiro.sh -r source_directory [-o rebuild_file]\n" 15 | echo -e "\033[1mCreate an encrypted txt\033[0m\n./papiro.sh -x [-o rebuild_file]\n" 16 | echo -e "Options" 17 | echo -e "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"; 18 | echo -e "\033[1m-z\033[0m\tZip the file(s) to reduce the number of QR Codes" 19 | echo -e "\033[1m-l\033[0m\tSet the QR Codes error correction level (L|M|Q|H); default is L(ow)" 20 | echo -e "\033[1m-o\033[0m\tSet the output filename" 21 | echo -e "\033[1m-a\033[0m\tAnonymous mode, don't annotate the original filename" 22 | echo -e "\033[1m-s\033[0m\tCreate a papiro of the script itself, useful for archiving with the encoded data" 23 | echo -e "\033[1m-h\033[0m\tShow this help" 24 | echo -e "\033[1m-d\033[0m\tDebug mode" 25 | echo -e "\nExamples" 26 | echo -e "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"; 27 | echo "Encode a file to qrcodes: ./papiro.sh -c myfile.jpg" 28 | echo "Zip and encode a file to qrcodes: ./papiro.sh -c divina-commedia.txt -z" 29 | echo "Encode a file with the best error correction: ./papiro.sh -c important-data.xml -lH " 30 | echo "Encode a directory to qrcodes (using a zip file): ./papiro.sh -c mydata/" 31 | echo "Decode a group of images to rebuild a file: ./papiro.sh -r photos/ -o myfile.jpg" 32 | } 33 | 34 | function show_help_hint { 35 | echo -e "Try ./papiro -h to see the help and some examples" 36 | } 37 | 38 | # Create a tmp dir 39 | work_dir=`mktemp -d` 40 | if [[ ! "$work_dir" || ! -d "$work_dir" ]]; then echo "Could not create the temp dir"; exit 1; fi 41 | 42 | echo "" # Blank line for readibility 43 | 44 | # Parse the flags 45 | while getopts "c:azl:xr:o:sdh" flag 46 | do 47 | case "${flag}" in 48 | c) encode_source=${OPTARG};; 49 | a) anonymous="ON";; 50 | z) zip="ON";; 51 | l) error_correction_level=${OPTARG};; 52 | x) new_secret="ON";; 53 | r) decode_dir=${OPTARG};; 54 | o) decode_output=${OPTARG};; 55 | s) encode_myself="ON";; 56 | d) debug="ON";; 57 | h) help="ON";; 58 | esac 59 | done 60 | 61 | # If invoked with the debug (-d) flag create a temp directory or empty if it alredy exists 62 | if [ -n "$debug" ]; then 63 | debug_dir="papiro-debug" 64 | echo -e "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"; 65 | echo "=> Debug mode is active"; 66 | mkdir -p $PWD/$debug_dir; 67 | rm -rf $PWD/$debug_dir/*; 68 | echo -e " Work_dir: $work_dir\n \033[1mRemember to clear it manually\033[0m" 69 | echo -e "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"; 70 | else 71 | # The work temp dir will be cleared automatically on the exit 72 | trap "rm -rf "$work_dir"" EXIT 73 | fi 74 | 75 | date_human=$(date "+%Y-%m-%d %H:%M") 76 | date_file=$(date "+%Y%m%d-%H%M%S") 77 | 78 | # If invoked with the -x flag, create a new encrypted vim file, then use it for the encoding 79 | if [ -n "$encode_myself" ]; then 80 | echo -e "=> Papiro self mode: I'm going to generate a papiro of myself!" 81 | encode_source=`basename $0` 82 | 83 | # Disable the zip in self mode 84 | if [ -n "$zip" ]; then 85 | echo -e "=> \033[1mWarning\033[0m: using the self mode with Zip is technically possibile but not very useful, because it is hard to rebuild a binary file. Let's go on with a plain txt :)" 86 | unset zip 87 | fi 88 | 89 | fi 90 | 91 | # If invoked with the -x flag, create a new encrypted vim file, then use it for the encoding 92 | if [ -n "$new_secret" ]; then 93 | encode_source="secret-$date_file" 94 | vim -xn $encode_source 95 | fi 96 | 97 | # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### 98 | # 99 | # ENCONDE 100 | # If invoked with the -c flag process the file to create the qrcodes-papiro pdf 101 | # 102 | # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### 103 | 104 | if [ -n "$encode_source" ]; then 105 | if ! [[ -f "$encode_source" || -d "$encode_source" ]]; then echo -e "Error: file not found"; show_help_hint; exit 2; fi 106 | 107 | # If the source is a directory automatically activate the zip mode 108 | if [ -d "$encode_source" ]; then echo "=> Directory detected: automatically activate the Zip mode"; zip="ON"; fi 109 | 110 | # If invoked with the anonymous flag (-a) set the label_file and pdf_file accor accordingly to avoid to store the filename 111 | if [ -n "$anonymous" ]; then 112 | echo "=> Anonymous mode" 113 | output_file="xxxxxxxxxxxxxxxx" 114 | label_file="***************" 115 | else 116 | output_file=$(basename -- $encode_source) 117 | label_file=$output_file 118 | 119 | fi 120 | 121 | # Set the split chunk size according to the error correction, default is L(ow) 122 | case $error_correction_level in 123 | "L" | "") 124 | error_correction_level="L" 125 | split_chunk=2953;; 126 | "M") 127 | split_chunk=2331;; 128 | "Q") 129 | split_chunk=1663;; 130 | "H") 131 | split_chunk=1273;; 132 | *) 133 | echo "Error: bad correction level code, valid values are L, M, Q, and H"; show_help_hint; 134 | exit 2 135 | ;; 136 | esac 137 | 138 | echo "=> Error correction level: $error_correction_level" 139 | 140 | if [ -n "$decode_output" ]; then 141 | pdf_file=$decode_output 142 | elif [ -n "$anonymous" ]; then 143 | pdf_file="qrcodes-$date_file.pdf" 144 | else 145 | pdf_file=$PWD/qrcodes-$label_file.pdf 146 | fi 147 | 148 | work_file="$work_dir/$output_file" 149 | cp -R $encode_source $work_file 150 | 151 | if [ -n "$zip" ]; then 152 | echo "=> Zip mode" 153 | # Cannot use the split (-s -sp) option because 64k is the minimum split size 154 | # TODO add -q optin for suppress output 155 | current_position=$PWD 156 | cd $work_dir 157 | zip -r -9 "$(basename -- $work_file.zip)" "$(basename -- $work_file)" 158 | work_file=$work_file.zip 159 | encode_source=$work_file 160 | cd "$current_position" 161 | fi 162 | 163 | # Calculate the checksum, it is included in the pdf for a future integrity check 164 | checksum=$(shasum -a 256 $encode_source | cut -f 1 -d ' ') 165 | echo "=> SHA256 signature: $checksum" 166 | 167 | echo "=> Encoding $encode_source to qrcodes" 168 | 169 | # Split the file in chunks of 1273 bytes. This is the max size for a qrcode v40 (177x177) with hight (H) error correction code level (ECC) 170 | # Check https://www.qrcode.com/en/about/version.html for more details 171 | split -b $split_chunk $work_file $work_file.split 172 | 173 | # Encode the files in a qrcode 177x177, hight correction mode 174 | for file in $work_file.split*; do qrencode --8bit -v 40 -l $error_correction_level -o $file.png -r $file; done 175 | 176 | # Get the qrcodes count 177 | total_files=`ls $work_file.split*.png | wc -l | sed "s/ *//g"` 178 | 179 | # Add a label to every qrcode 180 | counter=1; for file in $work_file.split*.png; do convert -comment "$label_file\n$date_human | $counter of $total_files parts" $file $file; counter=$((counter+1)); done 181 | if [ -n "$debug" ]; then cp $work_dir/*.png $PWD/$debug_dir/; fi 182 | 183 | # Create a multipage pdf and optimize its size 184 | title="\n\n$label_file | $date_human\nsha256: $checksum" 185 | if [ -n "$encode_myself" ]; then title="$title\nScan the qrcodes and merge the content in a unique text file, rename it to papiro.sh, run it"; fi 186 | montage -pointsize 20 -label '%c' $work_dir/*.png -title "$title" -geometry "1x1<" -tile 3x4 $pdf_file 187 | convert $pdf_file -border 40 -type bilevel -compress fax $pdf_file 188 | 189 | echo -e "\nYour Papiro is ready to print: $pdf_file" 190 | 191 | 192 | # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### 193 | # 194 | # DECODE 195 | # If invoked with the -r flag process the photos' dir to rebuild the original file 196 | # 197 | # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### 198 | 199 | elif [ -n "$decode_dir" ]; then 200 | if ! [[ -d "$decode_dir" ]]; then echo -e "Error: directory not found"; show_help_hint; exit 2; fi 201 | echo "Decoding $decode_dir/*" 202 | if [ -n "$decode_output" ]; then decode_output=$decode_output; else decode_output="decoded_file"; fi 203 | 204 | # Optimize the scanned images 205 | mkdir $work_dir/pics 206 | for file in $decode_dir/*; do convert "$file" -quiet -morphology open square:1 -threshold 50% "$work_dir/pics/$(basename -- $file).png"; done 207 | if [ -n "$debug" ]; then cp $work_dir/pics/* $PWD/$debug_dir/; fi 208 | 209 | # Scan optimized qrcodes and concatenate data to a unique file 210 | counter=1; for file in $work_dir/pics/* 211 | do 212 | zbar_output=$( (zbarimg --raw --oneshot -Sbinary -Sdisable -Sqr.enable "$file" >> $work_dir/restore) 2>&1 > /dev/null) 213 | if [[ $zbar_output == *"not detected"* ]]; then echo "Error: no content found in the qrcode #$counter, check the image quality"; exit 1; fi 214 | counter=$((counter+1)) 215 | done 216 | cp "$work_dir/restore" "$decode_output" 217 | 218 | # Add zip extension automatically 219 | file_type=`file -b $decode_output` 220 | if [[ $file_type == *"Zip"* ]] && [[ ! $decode_output == *"zip" ]]; then 221 | echo "=> Detected a Zip file: add the extension" 222 | mv "$decode_output" "$decode_output.zip" 223 | decode_output="$decode_output.zip" 224 | fi 225 | 226 | echo -e "\n=> File rebuild from papiro: $decode_output" 227 | echo -e "=> SHA256 signature: $(shasum -a 256 $decode_output | cut -f 1 -d ' ')" 228 | 229 | else 230 | 231 | show_help 232 | 233 | fi --------------------------------------------------------------------------------