diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8aa5d3b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = tab +trim_trailing_whitespace = true +end_of_line = lf +insert_final_newline = true + +[*.md] +indent_style = space +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82fbe68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# IntelliJ +.idea/ diff --git a/2fa-backup-sheet.sh b/2fa-backup-sheet.sh new file mode 100644 index 0000000..0f67877 --- /dev/null +++ b/2fa-backup-sheet.sh @@ -0,0 +1,161 @@ +#!/bin/bash + +# Exit immediately when statements or commands (outside of tests and conditions) fail (with a non-zero exit status) +set -e + +# Fail on references to unset variables or parameters +set -u + +# Make pipelines fail if any command within (i.e. not just the last one) fails +set -o pipefail + +# Don't return literal expressions with asterisks if a glob doesn't match but instead make the glob fail +shopt -s failglob + +canvasWidth=$((210 * 300 * 393701/10000000)) # mm * dpi * inch/mm +canvasHeight=$((297 * 300 * 393701/10000000)) # mm * dpi * inch/mm +backgroundColor="white" +paddingTop=$((20 * 300 * 393701/10000000)) # mm * dpi * inch/mm +paddingBottom=$((20 * 300 * 393701/10000000)) # mm * dpi * inch/mm +textTitle=${1:-} +textSubtitle=${3:-} +textDate=${4:-"$(date +'%Y-%m-%d')"} +textRecoveryCodes=${2:-} +pathKeyUriQrCode=${5:-} +linesTitle=$(echo "$textTitle" | sed 's/\\n/\n/g' | wc -l) +linesSubtitle=$(echo "$textSubtitle" | sed 's/\\n/\n/g' | wc -l) +linesDate=$(echo "$textDate" | sed 's/\\n/\n/g' | wc -l) +linesRecoveryCodes=$(echo "$textRecoveryCodes" | sed 's/\\n/\n/g' | wc -l) + +if [ -n "$textTitle" ]; then hasTitle=1; else hasTitle=0; fi +if [ -n "$textSubtitle" ]; then hasSubtitle=1; else hasSubtitle=0; fi +if [ -n "$textDate" ]; then hasDate=1; else hasDate=0; fi +if [ -n "$textRecoveryCodes" ]; then hasRecoveryCodes=1; else hasRecoveryCodes=0; fi +if [ -n "$pathKeyUriQrCode" ]; then hasKeyUriQrCode=1; else hasKeyUriQrCode=0; fi + +qrCodeSize=$((canvasWidth * 2/5)) +fontPathRegular=${6:-"/usr/share/fonts/truetype/ubuntu/UbuntuMono-R.ttf"} + +if [ -z "${6:-}" ]; then + fontPathBold=${7:-"/usr/share/fonts/truetype/ubuntu/UbuntuMono-B.ttf"} +else + fontPathBold=${7:-${fontPathRegular}} +fi + +fontScale=${8:-"100"} +fontSizeTitle=$((160 * fontScale/100)) +fontSizeSubtitle=$((112 * fontScale/100)) +fontSizeDate=$((112 * fontScale/100)) + +if [ "$hasKeyUriQrCode" -eq 1 ]; then + fontSizeRecoveryCodes=$((72 * fontScale/100)) +else + fontSizeRecoveryCodes=$((96 * fontScale/100)) +fi + +upperHeight=$((canvasHeight * 1/2 - qrCodeSize * 1/2)) + +if [ "$hasTitle" -eq 1 ] || [ "$hasSubtitle" -eq 1 ] || [ "$hasDate" -eq 1 ]; then + upperSpacing=$(((upperHeight - paddingTop - fontSizeTitle - fontSizeSubtitle - fontSizeDate) / (hasTitle + hasSubtitle + hasDate))) +else + upperSpacing=0 +fi + +lowerHeight=$((canvasHeight * 1/2 - qrCodeSize * 1/2)) + +if [ "$hasRecoveryCodes" -eq 1 ]; then + lowerSpacing=$(((lowerHeight - paddingBottom - fontSizeRecoveryCodes * linesRecoveryCodes * hasRecoveryCodes) / hasRecoveryCodes)) +else + lowerSpacing=0 +fi + +positionVerticalTitle=$paddingTop +positionVerticalSubtitle=$((paddingTop + (fontSizeTitle + upperSpacing) * hasTitle)) +positionVerticalDate=$((paddingTop + (fontSizeTitle + upperSpacing) * hasTitle + (fontSizeSubtitle + upperSpacing) * hasSubtitle)) +positionVerticalRecoveryCodes=$((upperHeight + qrCodeSize + lowerSpacing)) +outputFilename=${9:-"2fa-backup-$(date +'%Y%m%dT%H%M%S').png"} +outputFileBasename="${outputFilename%.*}" +outputFileExtension="${outputFilename##*.}" + +if [ "$#" -lt 2 ]; then + echo 'Usage:' + echo ' $ bash ./2fa-backup-sheet.sh <RECOVERY_CODES> [<SUBTITLE> [<DATE> [<OTP_KEY_URI_QR_PATH> [<FONT_PATH_REGULAR> [<FONT_PATH_BOLD> [<FONT_SCALE> [<OUTPUT_FILE_PATH>]]]]]]]' + echo '' + echo ' # e.g.' + echo '' + # shellcheck disable=SC2028 + echo ' # bash ./2fa-backup-sheet.sh "Google" "2589 0449\n5908 3492\n8491 4533\n2560 0808"' + echo ' # or' + # shellcheck disable=SC2028 + echo ' # bash ./2fa-backup-sheet.sh "Twitter" "3234 8651\n5962 8640\n2490 2239\n2873 6327\n5730 5927\n4371 8506\n9858 8718\n3884 9458\n9110 6833\n8815 4916" "john.doe@example.com" "" "key-uri-qr-code-screenshot.png" "RobotoMono-Regular.ttf" "RobotoMono-Bold.ttf" "75" "backup-sheet-twitter.png"' + exit 1 +fi + +if [ "$linesTitle" -gt 1 ]; then + echo ' ! You cannot use more than 1 line of text in the title' + exit 2 +fi + +if [ "$linesSubtitle" -gt 1 ]; then + echo ' ! You cannot use more than 1 line of text in the subtitle' + exit 3 +fi + +if [ "$linesDate" -gt 1 ]; then + echo ' ! You cannot use more than 1 line of text for the date' + exit 4 +fi + +if [ "$linesRecoveryCodes" -gt 10 ]; then + echo ' ! You cannot use more than 10 lines of text for the recovery codes' + exit 5 +fi + +if [ ! -r "$fontPathRegular" ]; then + echo ' ! Could not read path for regular font' + exit 6 +fi + +if [ ! -r "$fontPathBold" ]; then + echo ' ! Could not read path for bold font' + exit 7 +fi + +if [ "$hasKeyUriQrCode" -eq 1 ]; then + # Remove areas surrounding QR code itself + convert "$pathKeyUriQrCode" -bordercolor white -border 1x1 -white-threshold 50% -transparent white -trim +repage "${outputFileBasename}.2cdcd308.${outputFileExtension}" + # Resize the QR code to the desired size + convert "${outputFileBasename}.2cdcd308.${outputFileExtension}" -resize "${qrCodeSize}x${qrCodeSize}" "${outputFileBasename}.3f9de271.${outputFileExtension}" + # Remove temporary file + rm "${outputFileBasename}.2cdcd308.${outputFileExtension}" + # Place the QR code at the center of the overall canvas + convert "${outputFileBasename}.3f9de271.${outputFileExtension}" -gravity center -background "$backgroundColor" -extent "${canvasWidth}x${canvasHeight}" "${outputFileBasename}.f576a167.${outputFileExtension}" + # Remove temporary file + rm "${outputFileBasename}.3f9de271.${outputFileExtension}" +else + convert -size "${canvasWidth}x${canvasHeight}" canvas:"$backgroundColor" "${outputFileBasename}.f576a167.${outputFileExtension}" +fi + +# Write the title to the canvas +convert "${outputFileBasename}.f576a167.${outputFileExtension}" -gravity north -font "$fontPathBold" -pointsize "$fontSizeTitle" -annotate "+0+${positionVerticalTitle}" "$textTitle" "${outputFileBasename}.8822c7ef.${outputFileExtension}" +# Remove temporary file +rm "${outputFileBasename}.f576a167.${outputFileExtension}" +# Write the subtitle to the canvas +convert "${outputFileBasename}.8822c7ef.${outputFileExtension}" -gravity north -font "$fontPathRegular" -pointsize "$fontSizeSubtitle" -annotate "+0+${positionVerticalSubtitle}" "$textSubtitle" "${outputFileBasename}.25a0da4b.${outputFileExtension}" +# Remove temporary file +rm "${outputFileBasename}.8822c7ef.${outputFileExtension}" +# Write the date to the canvas +convert "${outputFileBasename}.25a0da4b.${outputFileExtension}" -gravity north -font "$fontPathRegular" -pointsize "$fontSizeDate" -annotate "+0+${positionVerticalDate}" "$textDate" "${outputFileBasename}.6a23dc41.${outputFileExtension}" +# Remove temporary file +rm "${outputFileBasename}.25a0da4b.${outputFileExtension}" +# Write the recovery codes to the canvas +convert "${outputFileBasename}.6a23dc41.${outputFileExtension}" -gravity north -font "$fontPathRegular" -pointsize "$fontSizeRecoveryCodes" -annotate "+0+${positionVerticalRecoveryCodes}" "$textRecoveryCodes" "${outputFileBasename}.345599c2.${outputFileExtension}" +# Remove temporary file +rm "${outputFileBasename}.6a23dc41.${outputFileExtension}" +# Move the result so that the output file has the desired filename +mv "${outputFileBasename}.345599c2.${outputFileExtension}" "$outputFilename" + +echo 'Written to:' +echo " $outputFilename" + +exit 0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ffa3629 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) delight.im (https://www.delight.im/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..76eadb2 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# 2FA-Backup-Sheet + +Create backup sheets for any service where you use two-factor authentication (2FA) + +Full | Minimal +:-------------------------:|:-------------------------: +<img src="sample-sheet-full.png?raw=true" alt="Full" width="300" /> | <img src="sample-sheet-minimal.png?raw=true" alt="Minimal" width="300" /> + +## Requirements + + * Unix + * Bash + * ImageMagick + +## Usage + +```bash +$ bash ./2fa-backup-sheet.sh \ + <TITLE> \ + <RECOVERY_CODES> \ + [<SUBTITLE> \ + [<DATE> \ + [<OTP_KEY_URI_QR_PATH> \ + [<FONT_PATH_REGULAR> \ + [<FONT_PATH_BOLD> \ + [<FONT_SCALE> \ + [<OUTPUT_FILE_PATH>]]]]]]] +``` + +### Examples + +```bash +$ bash ./2fa-backup-sheet.sh \ + "Google" \ + "2589 0449\n5908 3492\n8491 4533\n2560 0808" + +# or + +$ bash ./2fa-backup-sheet.sh \ + "Twitter" \ + "3234 8651\n5962 8640\n2490 2239\n2873 6327\n5730 5927\n4371 8506\n9858 8718\n3884 9458\n9110 6833\n8815 4916" \ + "john.doe@example.com" \ + "" \ + "key-uri-qr-code-screenshot.png" \ + "RobotoMono-Regular.ttf" \ + "RobotoMono-Bold.ttf" \ + "75" \ + "backup-sheet-twitter.png" +``` + +### Security + +Do *not* store the resulting backup sheets in the same place as your passwords, if such a place exists. For example, if you use a password manager to store your passwords, do *not* save your recovery codes or backup sheets there. Consider storing the backup sheets only in *printed form*, and put them in a safe place – perhaps even at two separate physical locations. + +If you want to include a QR code representing the key URI, which includes the shared secret or seed, you may want to take a screenshot of the QR code shown to you by the service in question. If you save the QR code directly, e.g. in your web browser, a new code (and thus secret) *may* be generated, depending on the service and the software you use. After generating the backup sheet, delete the screenshot that you took. + +## Contributing + +All contributions are welcome! If you wish to contribute, please create an issue first so that your feature, problem or question can be discussed. + +## License + +This project is licensed under the terms of the [MIT License](https://opensource.org/licenses/MIT). diff --git a/sample-qr.png b/sample-qr.png new file mode 100644 index 0000000..faba82d Binary files /dev/null and b/sample-qr.png differ diff --git a/sample-sheet-full.png b/sample-sheet-full.png new file mode 100644 index 0000000..65093bd Binary files /dev/null and b/sample-sheet-full.png differ diff --git a/sample-sheet-minimal.png b/sample-sheet-minimal.png new file mode 100644 index 0000000..0eec866 Binary files /dev/null and b/sample-sheet-minimal.png differ