Compare commits

...

21 Commits

Author SHA1 Message Date
scito
a60cbbb7bb ignore wheel files 2022-09-25 11:59:46 +02:00
scito
4546655cc5 add protoc upgrade script and update to protoc 21.6/protobuf 4.21.6 2022-09-25 11:54:22 +02:00
dependabot[bot]
39af5ab077 Bump protobuf from 4.21.5 to 4.21.6
Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 4.21.5 to 4.21.6.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/generate_changelog.py)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-25 09:27:43 +02:00
Roland Kurmann
be4d0d37db add default codeql-analysis.yml
This file enables security scans on GitHub.
2022-09-17 11:24:11 +02:00
scito
3933e6ed8a add #StandWithUkraine 2022-09-09 18:50:10 +02:00
scito
dbfd3464f2 save qr code to specific dir, improve help, add tests
- use metavar for files and dirs in help
- support several recursive dirs in saveqr
- add saveqr and debug tests
2022-09-09 13:15:22 +02:00
scito
fbefb3474c fix save_qr: dict notation is needed 2022-09-09 13:08:35 +02:00
scito
4baf406211 improve docu
- add help page to README.txt
- remove -p
- mention optional qrcode module
2022-09-08 21:26:19 +02:00
scito
cd2d3258d3 handle not encoded + in query params, fixes #15
- add debug level, by givein parameter -vv
- if the base64 string is not urlencoded, then + will be replaced by a space,
  what cannot be decoded anymore
  --> replace spaces back to plus
- add test
2022-09-07 21:58:03 +02:00
scito
df8b99dce4 add debug launch config for folder and workspace 2022-09-07 21:49:29 +02:00
scito
801c0e42d0 add technical spelling words, like TOTP 2022-09-07 19:59:38 +02:00
scito
d7f4533c99 enable base64 decode validation
Enable validation for analyzing #15
2022-09-07 19:36:10 +02:00
scito
9beb98693c README: link badges, add protobuf 2022-09-04 22:07:39 +02:00
scito
fbde835601 enable pytest in vscode and mention it in README 2022-09-04 19:02:36 +00:00
scito
fb4cee14da add VSCode devcontainer setup 2022-09-04 13:58:58 +00:00
scito
4027677b38 add vscode settings.json 2022-09-04 13:44:46 +02:00
scito
acab230436 improve README.md 2022-09-04 08:57:12 +02:00
scito
7fc69ea415 refactor to reduce complexity of extract_otps (main loop)
flake8 is happy now
2022-09-04 08:37:03 +02:00
scito
16d9fffc4a add test for verbose output 2022-09-04 08:16:20 +02:00
scito
33bba8848a add dependabot.yml 2022-09-04 01:30:45 +02:00
scito
2eaab5e3b5 update README and CI 2022-09-04 01:01:53 +02:00
19 changed files with 867 additions and 128 deletions

37
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.134.0/containers/python-3/.devcontainer/base.Dockerfile
# [Choice] Python version: 3, 3.10, ...
ARG VARIANT=3
FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT}
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# Remove imagemagick due to https://security-tracker.debian.org/tracker/CVE-2019-10131
&& apt-get purge -y imagemagick imagemagick-6-common \
# Install common packages, non-root user
# && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" \
# Clean up
&& apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
# [Option] Install Node.js
# ARG INSTALL_NODE="false"
# ARG NODE_VERSION="lts/*"
# RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] Allow the vscode user to pip install globally w/o sudo
# ENV PIP_TARGET=/usr/local/pip-global
# ENV PYTHONPATH=${PIP_TARGET}:${PYTHONPATH}
# ENV PATH=${PIP_TARGET}/bin:${PATH}
# RUN mkdir -p ${PIP_TARGET} \
# && chown vscode:root ${PIP_TARGET} \
# && echo "if [ \"\$(stat -c '%U' ${PIP_TARGET})\" != \"vscode\" ]; then chown -R vscode:root ${PIP_TARGET}; fi" \
# | tee -a /root/.bashrc /home/vscode/.bashrc /root/.zshrc >> /home/vscode/.zshrc
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
# COPY requirements.txt /tmp/pip-tmp/
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
# && rm -rf /tmp/pip-tmp
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update \
# && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>

View File

@@ -0,0 +1,22 @@
// Docu: https://containers.dev/implementors/json_reference/
{
"name": "Python 3",
"build": {
"dockerfile": "Dockerfile",
"context": "..",
//Update 'VARIANT' to pick a Python version: 3, 3.10, ...
"args": {
"VARIANT": "3.10"
}
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-python.python"
],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand":
"python -m pip install --upgrade pip; pip install -r requirements-dev.txt; pip install -r requirements.txt",
"postStartCommand": "echo 'Happy coding'"
// Comment out to connect as root instead.
// "remoteUser": "vscode"
}

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View File

@@ -1,4 +1,4 @@
name: extract_otp_secret_keys
name: tests
on: [push]
@@ -30,6 +30,3 @@ jobs:
- name: Test with pytest
run: |
pytest
- name: Test with unittest
run: |
python -m unittest

74
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '25 19 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

11
.gitignore vendored
View File

@@ -1,11 +1,16 @@
generated_python/__pycache__/
__pycache__/
qr/
venv/
*.csv
*.json
/*.csv
/*.json
!devbox.json
!example_output.json
!example_output.csv
!.github/
!.flake8
.vscode
!.vscode/settings.json
!.devcontainer/
!.devcontainer/*.json
*.whl

20
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"python.testing.pytestArgs": [
"."
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"cSpell.words": [
"devbox",
"HOTP",
"otpauth",
"pipenv",
"proto",
"protobuf",
"protoc",
"pytest",
"qrcode",
"TOTP",
"venv"
]
}

30
Pipfile.lock generated
View File

@@ -82,23 +82,23 @@
},
"protobuf": {
"hashes": [
"sha256:011c0f267e85f5d73750b6c25f0155d5db1e9443cd3590ab669a6221dd8fcdb0",
"sha256:3ec6f5b37935406bb9df9b277e79f8ed81d697146e07ef2ba8a5a272fb24b2c9",
"sha256:5310cbe761e87f0c1decce019d23f2101521d4dfff46034f8a12a53546036ec7",
"sha256:5e0b272217aad8971763960238c1a1e6a65d50ef7824e23300da97569a251c55",
"sha256:5e0ce02418ef03d7657a420ae8fd6fec4995ac713a3cb09164e95f694dbcf085",
"sha256:5eb0724615e90075f1d763983e708e1cef08e66b1891d8b8b6c33bc3b2f1a02b",
"sha256:7b6f22463e2d1053d03058b7b4ceca6e4ed4c14f8c286c32824df751137bf8e7",
"sha256:a7faa62b183d6a928e3daffd06af843b4287d16ef6e40f331575ecd236a7974d",
"sha256:b04484d6f42f48c57dd2737a72692f4c6987529cdd148fb5b8e5f616862a2e37",
"sha256:b52e7a522911a40445a5f588bd5b5e584291bfc5545e09b7060685e4b2ff814f",
"sha256:bf711b451212dc5b0fa45ae7dada07d8e71a4b0ff0bc8e4783ee145f47ac4f82",
"sha256:e5c5a2886ae48d22a9d32fbb9b6636a089af3cd26b706750258ce1ca96cc0116",
"sha256:eb1106e87e095628e96884a877a51cdb90087106ee693925ec0a300468a9be3a",
"sha256:ee04f5823ed98bb9a8c3b1dc503c49515e0172650875c3f76e225b223793a1f2"
"sha256:07a0bb9cc6114f16a39c866dc28b6e3d96fa4ffb9cc1033057412547e6e75cb9",
"sha256:308173d3e5a3528787bb8c93abea81d5a950bdce62840d9760effc84127fb39c",
"sha256:4143513c766db85b9d7c18dbf8339673c8a290131b2a0fe73855ab20770f72b0",
"sha256:49f88d56a9180dbb7f6199c920f5bb5c1dd0172f672983bb281298d57c2ac8eb",
"sha256:6b1040a5661cd5f6e610cbca9cfaa2a17d60e2bb545309bc1b278bb05be44bdd",
"sha256:77b355c8604fe285536155286b28b0c4cbc57cf81b08d8357bf34829ea982860",
"sha256:7a6cc8842257265bdfd6b74d088b829e44bcac3cca234c5fdd6052730017b9ea",
"sha256:80e6540381080715fddac12690ee42d087d0d17395f8d0078dfd6f1181e7be4c",
"sha256:8f9e60f7d44592c66e7b332b6a7b4b6e8d8b889393c79dbc3a91f815118f8eac",
"sha256:9666da97129138585b26afcb63ad4887f602e169cafe754a8258541c553b8b5d",
"sha256:aa29113ec901281f29d9d27b01193407a98aa9658b8a777b0325e6d97149f5ce",
"sha256:b6cea204865595a92a7b240e4b65bcaaca3ad5d2ce25d9db3756eba06041138e",
"sha256:ba596b9ffb85c909fcfe1b1a23136224ed678af3faf9912d3fa483d5f9813c4e",
"sha256:c7c864148a237f058c739ae7a05a2b403c0dfa4ce7d1f3e5213f352ad52d57c6"
],
"index": "pypi",
"version": "==4.21.5"
"version": "==4.21.6"
},
"qrcode": {
"hashes": [

View File

@@ -2,12 +2,16 @@
[![CI Status](https://github.com/scito/extract_otp_secret_keys/actions/workflows/ci.yml/badge.svg)](https://github.com/scito/extract_otp_secret_keys/actions/workflows/ci.yml)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/protobuf)
![GitHub Pipenv locked Python version](https://img.shields.io/github/pipenv/locked/python-version/scito/extract_otp_secret_keys)
![License](https://img.shields.io/github/license/scito/extract_otp_secret_keys)
[![GitHub Pipenv locked Python version](https://img.shields.io/github/pipenv/locked/python-version/scito/extract_otp_secret_keys)](https://github.com/scito/extract_otp_secret_keys/blob/master/Pipfile.lock)
![protobuf version](https://img.shields.io/badge/protobuf-4.21.6-informational)
[![License](https://img.shields.io/github/license/scito/extract_otp_secret_keys)](https://github.com/scito/extract_otp_secret_keys/blob/master/LICENSE)
[![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/scito/extract_otp_secret_keys?sort=semver&label=version)](https://github.com/scito/extract_otp_secret_keys/tags)
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua)
---
Extract two-factor authentication (2FA, TFA) secret keys from export QR codes of "Google Authenticator" app
Extract two-factor authentication (2FA, TFA) secret keys from export QR codes of "Google Authenticator" app.
The secret and otp values can be printed and exported to json or csv. The QR codes can be printed or saved as PNG images.
## Usage
@@ -16,7 +20,23 @@ Extract two-factor authentication (2FA, TFA) secret keys from export QR codes of
3. Save the captured QR codes in a text file. Save each QR code on a new line. (The captured QR codes look like `otpauth-migration://offline?data=...`)
4. Call this script with the file as input:
python extract_otp_secret_keys.py -p example_export.txt
python extract_otp_secret_keys.py example_export.txt
## Program help: arguments and options
<pre>usage: extract_otp_secret_keys.py [-h] [--json FILE] [--csv FILE] [--printqr] [--saveqr DIR] [--verbose] [--quiet] infile
positional arguments:
infile file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored
options:
-h, --help show this help message and exit
--json FILE, -j FILE export to json file
--csv FILE, -c FILE export to csv file
--printqr, -p print QR code(s) as text to the terminal (requires qrcode module)
--saveqr DIR, -s DIR save QR code(s) as images to the given folder (requires qrcode module)
--verbose, -v verbose output
--quiet, -q no stdout output</pre>
## Dependencies
@@ -24,7 +44,9 @@ Extract two-factor authentication (2FA, TFA) secret keys from export QR codes of
Known to work with
* Python 3.10.6, protobuf 4.21.5, qrcode 7.3.1, and pillow 9.2
* Python 3.10.7, protobuf 4.21.6, qrcode 7.3.1, and pillow 9.2
For protobuf versions 3.14.0 or similar or Python 3.6, use the extract_otp_secret_keys version 1.4.0.
### Optional
@@ -41,7 +63,7 @@ Command for regeneration of Python code from proto3 message definition file (onl
protoc --python_out=protobuf_generated_python google_auth.proto
The generated protobuf Python code was generated by protoc 21.5 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.5).
The generated protobuf Python code was generated by protoc 21.6 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.6).
## References
@@ -60,13 +82,26 @@ pipenv shell
python extract_otp_secret_keys.py example_export.txt
```
### Visual Studio Code Remote - Containers / VSCode devcontainer
You can you use [VSCode devcontainer](https://code.visualstudio.com/docs/remote/containers-tutorial) for running extract_otp_secret_keys.
Requirement: Docker
1. Start VSCode
2. Open extract_otp_secret_keys.code-workspace
3. Open VSCode command palette (Ctrl-Shift-P)
4. Type command "Remote-Containers: Reopen in Container"
5. Open integrated bash terminal in VSCode
6. Execute: python extract_otp_secret_keys.py example_export.txt
### venv
Alternatively, you can use a python virtual env for the dependencies:
python -m venv venv
. venv/bin/activate
pip install -r requirements-buildenv.txt
pip install -r requirements-dev.txt
pip install -r requirements.txt
The requirements\*.txt files contain all the dependencies (also the optional ones).
@@ -105,3 +140,28 @@ Run tests:
```
python -m unittest
```
### VSCode Setup
Setup for running the tests in VSCode.
1. Open VSCode command palette (Ctrl-Shift-P)
2. Type command "Python: Configure Tests"
3. Choose unittest or pytest. (pytest is recommended, both are supported)
4. Set ". Root" directory
## Maintenance
### Upgrade pip Packages
```
pip install -U -r requirements.txt
```
***
# #StandWithUkraine 🇺🇦
I have Ukrainian relatives and friends.
#RussiaInvadedUkraine on 24 of February 2022, at 05:00 the armed forces of the Russian Federation attacked Ukraine. Please, stand with Ukraine, stay tuned for updates on Ukraine's official sources and channels in English and support Ukraine in its fight for freedom and democracy in Europe.

View File

@@ -0,0 +1,25 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"python.testing.pytestEnabled": true
},
"launch": {
"version": "0.2.0",
"configurations": [
{
"name": "Python: extract_otp_secret_keys.py",
"type": "python",
"request": "launch",
"program": "extract_otp_secret_keys.py",
"args": [
"example_export.txt"
],
"console": "integratedTerminal"
},
]
}
}

View File

@@ -5,7 +5,7 @@
# 2. Read QR codes with QR code reader (e.g. with a second device)
# 3. Save the captured QR codes in a text file. Save each QR code on a new line. (The captured QR codes look like "otpauth-migration://offline?data=...")
# 4. Call this script with the file as input:
# python extract_otp_secret_keys.py -p example_export.txt
# python extract_otp_secret_keys.py example_export.txt
#
# Requirement:
# The protobuf package of Google for proto3 is required for running this script.
@@ -48,11 +48,106 @@ import sys
import csv
import json
from urllib.parse import parse_qs, urlencode, urlparse, quote
from os import path, mkdir
from os import path, makedirs
from re import compile as rcompile
import protobuf_generated_python.google_auth_pb2
def sys_main():
main(sys.argv[1:])
def main(sys_args):
global verbose, quiet
args = parse_args(sys_args)
verbose = args.verbose if args.verbose else 0
quiet = args.quiet
otps = extract_otps(args)
write_csv(args, otps)
write_json(args, otps)
def parse_args(sys_args):
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument('infile', help='file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored')
arg_parser.add_argument('--json', '-j', help='export to json file', metavar=('FILE'))
arg_parser.add_argument('--csv', '-c', help='export to csv file', metavar=('FILE'))
arg_parser.add_argument('--printqr', '-p', help='print QR code(s) as text to the terminal (requires qrcode module)', action='store_true')
arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the given folder (requires qrcode module)', metavar=('DIR'))
arg_parser.add_argument('--verbose', '-v', help='verbose output', action='count')
arg_parser.add_argument('--quiet', '-q', help='no stdout output', action='store_true')
args = arg_parser.parse_args(sys_args)
if args.verbose and args.quiet:
print("The arguments --verbose and --quite are mutual exclusive.")
sys.exit(1)
return args
def extract_otps(args):
global verbose, quiet
quiet = args.quiet
otps = []
i = j = 0
for line in (line.strip() for line in fileinput.input(args.infile)):
if verbose: print(line)
if line.startswith('#') or line == '': continue
i += 1
payload = get_payload_from_line(line, i, args)
# pylint: disable=no-member
for raw_otp in payload.otp_parameters:
j += 1
if verbose: print('\n{}. Secret Key'.format(j))
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
otp_type = get_enum_name_by_number(raw_otp, 'type')
otp_url = build_otp_url(secret, raw_otp)
otp = {
"name": raw_otp.name,
"secret": secret,
"issuer": raw_otp.issuer,
"type": otp_type,
"url": otp_url
}
if not quiet:
print_otp(otp)
if args.printqr:
print_qr(args, otp_url)
if args.saveqr:
save_qr(otp, args, j)
if not quiet:
print()
otps.append(otp)
return otps
def get_payload_from_line(line, i, args):
global verbose
if not line.startswith('otpauth-migration://'):
print('\nWARN: line is not a otpauth-migration:// URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(args.infile, line))
parsed_url = urlparse(line)
if verbose > 1: print('\nDEBUG: parsed_url={}'.format(parsed_url))
params = parse_qs(parsed_url.query, strict_parsing=True)
if verbose > 1: print('\nDEBUG: querystring params={}'.format(params))
if 'data' not in params:
print('\nERROR: no data query parameter in input URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(args.infile, line))
sys.exit(1)
data_base64 = params['data'][0]
if verbose > 1: print('\nDEBUG: data_base64={}'.format(data_base64))
data_base64_fixed = data_base64.replace(' ', '+')
if verbose > 1: print('\nDEBUG: data_base64_fixed={}'.format(data_base64))
data = base64.b64decode(data_base64_fixed, validate=True)
payload = protobuf_generated_python.google_auth_pb2.MigrationPayload()
payload.ParseFromString(data)
if verbose:
print('\n{}. Payload Line'.format(i), payload, sep='\n')
return payload
# https://stackoverflow.com/questions/40226049/find-enums-listed-in-python-descriptor-for-protobuf
def get_enum_name_by_number(parent, field_name):
field_value = getattr(parent, field_name)
@@ -63,7 +158,34 @@ def convert_secret_from_bytes_to_base32_str(bytes):
return str(base64.b32encode(bytes), 'utf-8').replace('=', '')
def save_qr(args, data, name):
def build_otp_url(secret, raw_otp):
url_params = {'secret': secret}
if raw_otp.type == 1: url_params['counter'] = raw_otp.counter
if raw_otp.issuer: url_params['issuer'] = raw_otp.issuer
otp_url = 'otpauth://{}/{}?'.format('totp' if raw_otp.type == 2 else 'hotp', quote(raw_otp.name)) + urlencode(url_params)
return otp_url
def print_otp(otp):
print('Name: {}'.format(otp['name']))
print('Secret: {}'.format(otp['secret']))
if otp['issuer']: print('Issuer: {}'.format(otp['issuer']))
print('Type: {}'.format(otp['type']))
if verbose:
print(otp['url'])
def save_qr(otp, args, j):
dir = args.saveqr
if not (path.exists(dir)): makedirs(dir, exist_ok=True)
pattern = rcompile(r'[\W_]+')
file_otp_name = pattern.sub('', otp['name'])
file_otp_issuer = pattern.sub('', otp['issuer'])
save_qr_file(args, otp['url'], '{}/{}-{}{}.png'.format(dir, j, file_otp_name, '-' + file_otp_issuer if file_otp_issuer else ''))
return file_otp_issuer
def save_qr_file(args, data, name):
from qrcode import QRCode
global verbose
qr = QRCode()
@@ -80,95 +202,6 @@ def print_qr(args, data):
qr.print_ascii()
def parse_args(sys_args):
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument('--verbose', '-v', help='verbose output', action='store_true')
arg_parser.add_argument('--quiet', '-q', help='no stdout output', action='store_true')
arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the "qr" subfolder', action='store_true')
arg_parser.add_argument('--printqr', '-p', help='print QR code(s) as text to the terminal', action='store_true')
arg_parser.add_argument('--json', '-j', help='export to json file')
arg_parser.add_argument('--csv', '-c', help='export to csv file')
arg_parser.add_argument('infile', help='file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored')
args = arg_parser.parse_args(sys_args)
if args.verbose and args.quiet:
print("The arguments --verbose and --quite are mutual exclusive.")
sys.exit(1)
return args
def sys_main():
main(sys.argv[1:])
def main(sys_args):
global verbose, quiet
args = parse_args(sys_args)
verbose = args.verbose
quiet = args.quiet
otps = extract_otps(args)
write_csv(args, otps)
write_json(args, otps)
def extract_otps(args):
global verbose, quiet
quiet = args.quiet
otps = []
i = j = 0
for line in (line.strip() for line in fileinput.input(args.infile)):
if verbose: print(line)
if line.startswith('#') or line == '': continue
if not line.startswith('otpauth-migration://'): print('\nWARN: line is not a otpauth-migration:// URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(args.infile, line))
parsed_url = urlparse(line)
params = parse_qs(parsed_url.query)
if 'data' not in params:
print('\nERROR: no data query parameter in input URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(args.infile, line))
sys.exit(1)
data_encoded = params['data'][0]
data = base64.b64decode(data_encoded)
payload = protobuf_generated_python.google_auth_pb2.MigrationPayload()
payload.ParseFromString(data)
i += 1
if verbose: print('\n{}. Payload Line'.format(i), payload, sep='\n')
# pylint: disable=no-member
for otp in payload.otp_parameters:
j += 1
if verbose: print('\n{}. Secret Key'.format(j))
if not quiet: print('Name: {}'.format(otp.name))
secret = convert_secret_from_bytes_to_base32_str(otp.secret)
if not quiet: print('Secret: {}'.format(secret))
if otp.issuer and not quiet: print('Issuer: {}'.format(otp.issuer))
otp_type = get_enum_name_by_number(otp, 'type')
if not quiet: print('Type: {}'.format(otp_type))
url_params = {'secret': secret}
if otp.type == 1: url_params['counter'] = otp.counter
if otp.issuer: url_params['issuer'] = otp.issuer
otp_url = 'otpauth://{}/{}?'.format('totp' if otp.type == 2 else 'hotp', quote(otp.name)) + urlencode(url_params)
if verbose: print(otp_url)
if args.printqr:
print_qr(args, otp_url)
if args.saveqr:
if not (path.exists('qr')): mkdir('qr')
pattern = rcompile(r'[\W_]+')
file_otp_name = pattern.sub('', otp.name)
file_otp_issuer = pattern.sub('', otp.issuer)
save_qr(args, otp_url, 'qr/{}-{}{}.png'.format(j, file_otp_name, '-' + file_otp_issuer if file_otp_issuer else ''))
if not quiet: print()
otps.append({
"name": otp.name,
"secret": secret,
"issuer": otp.issuer,
"type": otp_type,
"url": otp_url
})
return otps
def write_csv(args, otps):
global verbose, quiet
if args.csv and len(otps) > 0:

View File

@@ -1 +0,0 @@
wheel

4
requirements-dev.txt Normal file
View File

@@ -0,0 +1,4 @@
wheel
pytest
flake8
pylint

View File

@@ -0,0 +1,89 @@
# 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/
# Secret key: 7KSQL2JTUDIS5EF65KLMRQIIGY
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B
1. Payload Line
otp_parameters {
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
name: "pi@raspberrypi"
issuer: "raspberrypi"
algorithm: ALGO_SHA1
digits: 1
type: OTP_TOTP
}
version: 1
batch_size: 1
batch_id: -1320898453
1. Secret Key
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: OTP_TOTP
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D
2. Payload Line
otp_parameters {
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
name: "pi@raspberrypi"
algorithm: ALGO_SHA1
digits: 1
type: OTP_TOTP
}
version: 1
batch_size: 1
batch_id: -2094403140
2. Secret Key
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: OTP_TOTP
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B
3. Payload Line
otp_parameters {
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
name: "pi@raspberrypi"
algorithm: ALGO_SHA1
digits: 1
type: OTP_TOTP
}
otp_parameters {
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
name: "pi@raspberrypi"
issuer: "raspberrypi"
algorithm: ALGO_SHA1
digits: 1
type: OTP_TOTP
}
version: 1
batch_size: 1
batch_id: -1822886384
3. Secret Key
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: OTP_TOTP
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
4. Secret Key
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: OTP_TOTP
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi

View File

@@ -0,0 +1 @@
otpauth-migration://offline?data=ClEKFAciUeGF4aS6IDCvMv99ySZ1ekKsEiVTZXJlbml0eUxhYnM6dGVzdDFAc2VyZW5pdHlsYWJzLmNvLnVrGgxTZXJlbml0eUxhYnMgASgBMAIKUQoUkIY8/fbrHZWTb4CBln18lvqt0HcSJVNlcmVuaXR5TGFiczp0ZXN0MkBzZXJlbml0eWxhYnMuY28udWsaDFNlcmVuaXR5TGFicyABKAEwAgpRChScf+1/Ua4d4gCY0W/7fj9VBkM9PBIlU2VyZW5pdHlMYWJzOnRlc3QzQHNlcmVuaXR5bGFicy5jby51axoMU2VyZW5pdHlMYWJzIAEoATACClEKFG6Qu0ryTSFA/l5rmvTIXtNeb5LtEiVTZXJlbml0eUxhYnM6dGVzdDRAc2VyZW5pdHlsYWJzLmNvLnVrGgxTZXJlbml0eUxhYnMgASgBMAIQARgBIAAogtTa1vz/////AQ==

View File

@@ -18,12 +18,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from utils import read_csv, read_json, remove_file, read_file_to_str
from utils import read_csv, read_json, remove_file, remove_dir_with_files, read_file_to_str
from os import path
from pytest import raises
import extract_otp_secret_keys
def test_extract_csv():
def test_extract_csv(capsys):
# Arrange
cleanup()
@@ -36,11 +38,16 @@ def test_extract_csv():
assert actual_csv == expected_csv
captured = capsys.readouterr()
assert captured.out == ''
assert captured.err == ''
# Clean up
cleanup()
def test_extract_json():
def test_extract_json(capsys):
# Arrange
cleanup()
@@ -53,6 +60,11 @@ def test_extract_json():
assert actual_json == expected_json
captured = capsys.readouterr()
assert captured.out == ''
assert captured.err == ''
# Clean up
cleanup()
@@ -88,6 +100,39 @@ Type: OTP_TOTP
assert captured.err == ''
def test_extract_not_encoded_plus(capsys):
# Act
extract_otp_secret_keys.main(['test/test_plus_problem_export.txt'])
# Assert
captured = capsys.readouterr()
expected_stdout = '''Name: SerenityLabs:test1@serenitylabs.co.uk
Secret: A4RFDYMF4GSLUIBQV4ZP67OJEZ2XUQVM
Issuer: SerenityLabs
Type: OTP_TOTP
Name: SerenityLabs:test2@serenitylabs.co.uk
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
Issuer: SerenityLabs
Type: OTP_TOTP
Name: SerenityLabs:test3@serenitylabs.co.uk
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
Issuer: SerenityLabs
Type: OTP_TOTP
Name: SerenityLabs:test4@serenitylabs.co.uk
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
Issuer: SerenityLabs
Type: OTP_TOTP
'''
assert captured.out == expected_stdout
assert captured.err == ''
def test_extract_printqr(capsys):
# Act
extract_otp_secret_keys.main(['-p', 'example_export.txt'])
@@ -101,6 +146,71 @@ def test_extract_printqr(capsys):
assert captured.err == ''
def test_extract_saveqr(capsys):
# Arrange
cleanup()
# Act
extract_otp_secret_keys.main(['-q', '-s', 'testout/qr/', 'example_export.txt'])
# Assert
captured = capsys.readouterr()
assert captured.out == ''
assert captured.err == ''
assert path.isfile('testout/qr/1-piraspberrypi-raspberrypi.png')
assert path.isfile('testout/qr/2-piraspberrypi.png')
assert path.isfile('testout/qr/3-piraspberrypi.png')
assert path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png')
# Clean up
cleanup()
def test_extract_verbose(capsys):
# Act
extract_otp_secret_keys.main(['-v', 'example_export.txt'])
# Assert
captured = capsys.readouterr()
expected_stdout = read_file_to_str('test/print_verbose_output.txt')
assert captured.out == expected_stdout
assert captured.err == ''
def test_extract_debug(capsys):
# Act
extract_otp_secret_keys.main(['-vv', 'example_export.txt'])
# Assert
captured = capsys.readouterr()
expected_stdout = read_file_to_str('test/print_verbose_output.txt')
assert len(captured.out) > len(expected_stdout)
assert "DEBUG: " in captured.out
assert captured.err == ''
def test_extract_help(capsys):
with raises(SystemExit) as pytest_wrapped_e:
# Act
extract_otp_secret_keys.main(['-h'])
# Assert
captured = capsys.readouterr()
assert len(captured.out) > 0
assert "-h, --help" in captured.out and "--verbose, -v" in captured.out
assert captured.err == ''
assert pytest_wrapped_e.type == SystemExit
assert pytest_wrapped_e.value.code == 0
def cleanup():
remove_file('test_example_output.csv')
remove_file('test_example_output.json')
remove_dir_with_files('testout/')

View File

@@ -21,7 +21,8 @@
import unittest
import io
from contextlib import redirect_stdout
from utils import read_csv, read_json, remove_file, Capturing, read_file_to_str
from utils import read_csv, read_json, remove_file, remove_dir_with_files, Capturing, read_file_to_str
from os import path
import extract_otp_secret_keys
@@ -95,6 +96,35 @@ Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: OTP_TOTP
'''
self.assertEqual(actual_output, expected_output)
def test_extract_not_encoded_plus(self):
out = io.StringIO()
with redirect_stdout(out):
extract_otp_secret_keys.main(['test/test_plus_problem_export.txt'])
actual_output = out.getvalue()
expected_output = '''Name: SerenityLabs:test1@serenitylabs.co.uk
Secret: A4RFDYMF4GSLUIBQV4ZP67OJEZ2XUQVM
Issuer: SerenityLabs
Type: OTP_TOTP
Name: SerenityLabs:test2@serenitylabs.co.uk
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
Issuer: SerenityLabs
Type: OTP_TOTP
Name: SerenityLabs:test3@serenitylabs.co.uk
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
Issuer: SerenityLabs
Type: OTP_TOTP
Name: SerenityLabs:test4@serenitylabs.co.uk
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
Issuer: SerenityLabs
Type: OTP_TOTP
'''
self.assertEqual(actual_output, expected_output)
@@ -108,6 +138,48 @@ Type: OTP_TOTP
self.assertEqual(actual_output, expected_output)
def test_extract_saveqr(self):
extract_otp_secret_keys.main(['-q', '-s', 'testout/qr/', 'example_export.txt'])
self.assertTrue(path.isfile('testout/qr/1-piraspberrypi-raspberrypi.png'))
self.assertTrue(path.isfile('testout/qr/2-piraspberrypi.png'))
self.assertTrue(path.isfile('testout/qr/3-piraspberrypi.png'))
self.assertTrue(path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png'))
def test_extract_verbose(self):
out = io.StringIO()
with redirect_stdout(out):
extract_otp_secret_keys.main(['-v', 'example_export.txt'])
actual_output = out.getvalue()
expected_output = read_file_to_str('test/print_verbose_output.txt')
self.assertEqual(actual_output, expected_output)
def test_extract_debug(self):
out = io.StringIO()
with redirect_stdout(out):
extract_otp_secret_keys.main(['-vv', 'example_export.txt'])
actual_output = out.getvalue()
expected_stdout = read_file_to_str('test/print_verbose_output.txt')
self.assertGreater(len(actual_output), len(expected_stdout))
self.assertTrue("DEBUG: " in actual_output)
def test_extract_help(self):
out = io.StringIO()
with redirect_stdout(out):
try:
extract_otp_secret_keys.main(['-h'])
except SystemExit:
pass
actual_output = out.getvalue()
self.assertGreater(len(actual_output), 0)
self.assertTrue("-h, --help" in actual_output and "--verbose, -v" in actual_output)
def setUp(self):
self.cleanup()
@@ -117,6 +189,7 @@ Type: OTP_TOTP
def cleanup(self):
remove_file('test_example_output.csv')
remove_file('test_example_output.json')
remove_dir_with_files('testout/')
if __name__ == '__main__':

174
upgrade_protoc.sh Executable file
View File

@@ -0,0 +1,174 @@
#!/bin/bash
# Upgrades Protoc from https://github.com/protocolbuffers/protobuf/releases
black='\e[0;30m'
blackBold='\e[1;30m'
blackBackground='\e[1;40m'
red='\e[0;31m'
redBold='\e[1;31m'
redBackground='\e[0;41m'
green='\e[0;32m'
greenBold='\e[1;32m'
greenBackground='\e[0;42m'
yellow='\e[0;33m'
yellowBold='\e[1;33m'
yellowBackground='\e[0;43m'
blue='\e[0;34m'
blueBold='\e[1;34m'
blueBackground='\e[0;44m'
magenta='\e[0;35m'
magentaBold='\e[1;35m'
magentaBackground='\e[0;45m'
cyan='\e[0;36m'
cyanBold='\e[1;36m'
cyanBackground='\e[0;46m'
white='\e[0;37m'
whiteBold='\e[1;37m'
whiteBackground='\e[0;47m'
reset='\e[0m'
abort() {
echo '
***************
*** ABORTED ***
***************
' >&2
echo "An error occurred on line $1. Exiting..." >&2
date -Iseconds >&2
exit 1
}
trap 'abort $LINENO' ERR
set -e -o pipefail
quit() {
trap : 0
exit 0
}
# Asks if [Yn] if script shoud continue, otherwise exit 1
# $1: msg or nothing
# Example call 1: askContinueYn
# Example call 1: askContinueYn "Backup DB?"
askContinueYn() {
if [[ $1 ]]; then
msg="$1 "
else
msg=""
fi
# http://stackoverflow.com/questions/3231804/in-bash-how-to-add-are-you-sure-y-n-to-any-command-or-alias
read -e -p "${msg}Continue? [Y/n] " response
response=${response,,} # tolower
if [[ $response =~ ^(yes|y|)$ ]] ; then
# echo ""
# OK
:
else
echo "Aborted"
exit 1
fi
}
# Reference: https://gist.github.com/steinwaywhw/a4cd19cda655b8249d908261a62687f8
echo "Checking Protoc version..."
VERSION=$(curl -sL https://github.com/protocolbuffers/protobuf/releases/latest | grep -E "<title>" | perl -pe's%.*Protocol Buffers v(\d+\.\d+(\.\d+)?).*%\1%')
BASEVERSION=4
echo
interactive=true
check_version=true
while test $# -gt 0; do
case $1 in
-h|--help)
echo "Upgrade Protoc"
echo
echo "$0 [options]"
echo
echo "Options:"
echo "-a Automatic mode"
echo "-C Ignore version check"
echo "-h, --help Help"
quit
;;
-a)
interactive=false
shift
;;
-C)
check_version=false
shift
;;
esac
done
BIN="$HOME/bin"
DOWNLOADS="$HOME/downloads"
DEST="protoc"
OLDVERSION=$(cat $BIN/$DEST/.VERSION.txt || echo "")
echo -e "\nUpgrade Protoc $VERSION\n"
echo -e "Current version: $OLDVERSION\n"
if [ "$OLDVERSION" = "$VERSION" ] && $check_version; then
echo -e "\nVersion has not changed. Quit"
quit
fi
NAME="protoc-$VERSION"
ARCHIVE="$NAME.zip"
mkdir -p $DOWNLOADS
# https://github.com/protocolbuffers/protobuf/releases/download/v21.6/protoc-21.6-linux-x86_64.zip
cmd="wget --trust-server-names https://github.com/protocolbuffers/protobuf/releases/download/v$VERSION/protoc-$VERSION-linux-x86_64.zip -O $DOWNLOADS/$ARCHIVE"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="echo -e '\nSize [Byte]'; stat --printf='%s\n' $DOWNLOADS/$ARCHIVE; echo -e '\nMD5'; md5sum $DOWNLOADS/$ARCHIVE; echo -e '\nSHA256'; sha256sum $DOWNLOADS/$ARCHIVE;"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="mkdir -p $BIN/$NAME; unzip $DOWNLOADS/$ARCHIVE -d $BIN/$NAME"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="echo $VERSION > $BIN/$NAME/.VERSION.txt; echo $VERSION > $BIN/$NAME/.VERSION_$VERSION.txt"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="[ -d $BIN/$DEST.old ] && rm -rf $BIN/$DEST.old || echo 'No old dir to delete'"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="[ -d $BIN/$DEST ] && mv -iT $BIN/$DEST $BIN/$DEST.old || echo 'No previous dir to keep'"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="mv -iT $BIN/$NAME $BIN/$DEST"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="rm $DOWNLOADS/$ARCHIVE"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="$BIN/$DEST/bin/protoc --python_out=protobuf_generated_python google_auth.proto"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="pip install -U -r requirements.txt"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="pytest"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="perl -i -pe 's%proto(buf|c)([- ])(\d\.)?$OLDVERSION%proto\$1\$2\${3}$VERSION%g' README.md && perl -i -pe 's%(protobuf/releases/tag/v)$OLDVERSION%\${1}$VERSION%g' README.md"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
quit

View File

@@ -16,6 +16,7 @@
import csv
import json
import os
import shutil
from io import StringIO
import sys
@@ -38,8 +39,12 @@ with Capturing() as output:
sys.stdout = self._stdout
def remove_file(filename):
if os.path.exists(filename): os.remove(filename)
def remove_file(file):
if os.path.isfile(file): os.remove(file)
def remove_dir_with_files(dir):
if os.path.exists(dir): shutil.rmtree(dir)
def read_csv(filename):