mirror of
https://github.com/scito/extract_otp_secrets.git
synced 2025-12-15 11:21:43 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9beb98693c | ||
|
|
fbde835601 | ||
|
|
fb4cee14da | ||
|
|
4027677b38 | ||
|
|
acab230436 | ||
|
|
7fc69ea415 | ||
|
|
16d9fffc4a | ||
|
|
33bba8848a | ||
|
|
2eaab5e3b5 |
37
.devcontainer/Dockerfile
Normal file
37
.devcontainer/Dockerfile
Normal 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>
|
||||||
22
.devcontainer/devcontainer.json
Normal file
22
.devcontainer/devcontainer.json
Normal 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
11
.github/dependabot.yml
vendored
Normal 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"
|
||||||
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: extract_otp_secret_keys
|
name: tests
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
|
|
||||||
@@ -30,6 +30,3 @@ jobs:
|
|||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
pytest
|
pytest
|
||||||
- name: Test with unittest
|
|
||||||
run: |
|
|
||||||
python -m unittest
|
|
||||||
|
|||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,11 +1,15 @@
|
|||||||
generated_python/__pycache__/
|
__pycache__/
|
||||||
qr/
|
qr/
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
*.csv
|
/*.csv
|
||||||
*.json
|
/*.json
|
||||||
!devbox.json
|
!devbox.json
|
||||||
!example_output.json
|
!example_output.json
|
||||||
!example_output.csv
|
!example_output.csv
|
||||||
!.github/
|
!.github/
|
||||||
!.flake8
|
!.flake8
|
||||||
|
.vscode
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.devcontainer/
|
||||||
|
!.devcontainer/*.json
|
||||||
|
|||||||
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"python.testing.pytestArgs": [
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
"python.testing.unittestEnabled": false,
|
||||||
|
"python.testing.pytestEnabled": true
|
||||||
|
}
|
||||||
35
README.md
35
README.md
@@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
[](https://github.com/scito/extract_otp_secret_keys/actions/workflows/ci.yml)
|
[](https://github.com/scito/extract_otp_secret_keys/actions/workflows/ci.yml)
|
||||||

|

|
||||||

|
[](https://github.com/scito/extract_otp_secret_keys/blob/master/Pipfile.lock)
|
||||||

|

|
||||||
|
[](https://github.com/scito/extract_otp_secret_keys/blob/master/LICENSE)
|
||||||
|
[](https://github.com/scito/extract_otp_secret_keys/tags)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
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
|
## Usage
|
||||||
|
|
||||||
@@ -26,6 +29,8 @@ Known to work with
|
|||||||
|
|
||||||
* Python 3.10.6, protobuf 4.21.5, qrcode 7.3.1, and pillow 9.2
|
* Python 3.10.6, protobuf 4.21.5, 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
|
### Optional
|
||||||
|
|
||||||
For printing QR codes, the qrcode module is required, otherwise it can be omitted.
|
For printing QR codes, the qrcode module is required, otherwise it can be omitted.
|
||||||
@@ -60,13 +65,26 @@ pipenv shell
|
|||||||
python extract_otp_secret_keys.py example_export.txt
|
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
|
### venv
|
||||||
|
|
||||||
Alternatively, you can use a python virtual env for the dependencies:
|
Alternatively, you can use a python virtual env for the dependencies:
|
||||||
|
|
||||||
python -m venv venv
|
python -m venv venv
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pip install -r requirements-buildenv.txt
|
pip install -r requirements-dev.txt
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
The requirements\*.txt files contain all the dependencies (also the optional ones).
|
The requirements\*.txt files contain all the dependencies (also the optional ones).
|
||||||
@@ -105,3 +123,12 @@ Run tests:
|
|||||||
```
|
```
|
||||||
python -m unittest
|
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
|
||||||
|
|||||||
10
extract_otp_secret_keys.code-workspace
Normal file
10
extract_otp_secret_keys.code-workspace
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"python.testing.pytestEnabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,31 +53,19 @@ from re import compile as rcompile
|
|||||||
import protobuf_generated_python.google_auth_pb2
|
import protobuf_generated_python.google_auth_pb2
|
||||||
|
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/40226049/find-enums-listed-in-python-descriptor-for-protobuf
|
def sys_main():
|
||||||
def get_enum_name_by_number(parent, field_name):
|
main(sys.argv[1:])
|
||||||
field_value = getattr(parent, field_name)
|
|
||||||
return parent.DESCRIPTOR.fields_by_name[field_name].enum_type.values_by_number.get(field_value).name
|
|
||||||
|
|
||||||
|
|
||||||
def convert_secret_from_bytes_to_base32_str(bytes):
|
def main(sys_args):
|
||||||
return str(base64.b32encode(bytes), 'utf-8').replace('=', '')
|
global verbose, quiet
|
||||||
|
args = parse_args(sys_args)
|
||||||
|
verbose = args.verbose
|
||||||
|
quiet = args.quiet
|
||||||
|
|
||||||
|
otps = extract_otps(args)
|
||||||
def save_qr(args, data, name):
|
write_csv(args, otps)
|
||||||
from qrcode import QRCode
|
write_json(args, otps)
|
||||||
global verbose
|
|
||||||
qr = QRCode()
|
|
||||||
qr.add_data(data)
|
|
||||||
img = qr.make_image(fill_color='black', back_color='white')
|
|
||||||
if verbose: print('Saving to {}'.format(name))
|
|
||||||
img.save(name)
|
|
||||||
|
|
||||||
|
|
||||||
def print_qr(args, data):
|
|
||||||
from qrcode import QRCode
|
|
||||||
qr = QRCode()
|
|
||||||
qr.add_data(data)
|
|
||||||
qr.print_ascii()
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args(sys_args):
|
def parse_args(sys_args):
|
||||||
@@ -96,21 +84,6 @@ def parse_args(sys_args):
|
|||||||
return args
|
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):
|
def extract_otps(args):
|
||||||
global verbose, quiet
|
global verbose, quiet
|
||||||
quiet = args.quiet
|
quiet = args.quiet
|
||||||
@@ -121,54 +94,107 @@ def extract_otps(args):
|
|||||||
for line in (line.strip() for line in fileinput.input(args.infile)):
|
for line in (line.strip() for line in fileinput.input(args.infile)):
|
||||||
if verbose: print(line)
|
if verbose: print(line)
|
||||||
if line.startswith('#') or line == '': continue
|
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
|
i += 1
|
||||||
if verbose: print('\n{}. Payload Line'.format(i), payload, sep='\n')
|
payload = get_payload_from_line(line, i, args)
|
||||||
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
for otp in payload.otp_parameters:
|
for raw_otp in payload.otp_parameters:
|
||||||
j += 1
|
j += 1
|
||||||
if verbose: print('\n{}. Secret Key'.format(j))
|
if verbose: print('\n{}. Secret Key'.format(j))
|
||||||
if not quiet: print('Name: {}'.format(otp.name))
|
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
|
||||||
secret = convert_secret_from_bytes_to_base32_str(otp.secret)
|
otp_type = get_enum_name_by_number(raw_otp, 'type')
|
||||||
if not quiet: print('Secret: {}'.format(secret))
|
otp_url = build_otp_url(secret, raw_otp)
|
||||||
if otp.issuer and not quiet: print('Issuer: {}'.format(otp.issuer))
|
otp = {
|
||||||
otp_type = get_enum_name_by_number(otp, 'type')
|
"name": raw_otp.name,
|
||||||
if not quiet: print('Type: {}'.format(otp_type))
|
"secret": secret,
|
||||||
url_params = {'secret': secret}
|
"issuer": raw_otp.issuer,
|
||||||
if otp.type == 1: url_params['counter'] = otp.counter
|
"type": otp_type,
|
||||||
if otp.issuer: url_params['issuer'] = otp.issuer
|
"url": otp_url
|
||||||
otp_url = 'otpauth://{}/{}?'.format('totp' if otp.type == 2 else 'hotp', quote(otp.name)) + urlencode(url_params)
|
}
|
||||||
if verbose: print(otp_url)
|
if not quiet:
|
||||||
|
print_otp(otp)
|
||||||
if args.printqr:
|
if args.printqr:
|
||||||
print_qr(args, otp_url)
|
print_qr(args, otp_url)
|
||||||
if args.saveqr:
|
if args.saveqr:
|
||||||
if not (path.exists('qr')): mkdir('qr')
|
save_qr(otp, args, j)
|
||||||
pattern = rcompile(r'[\W_]+')
|
if not quiet:
|
||||||
file_otp_name = pattern.sub('', otp.name)
|
print()
|
||||||
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({
|
otps.append(otp)
|
||||||
"name": otp.name,
|
|
||||||
"secret": secret,
|
|
||||||
"issuer": otp.issuer,
|
|
||||||
"type": otp_type,
|
|
||||||
"url": otp_url
|
|
||||||
})
|
|
||||||
return otps
|
return otps
|
||||||
|
|
||||||
|
|
||||||
|
def get_payload_from_line(line, i, args):
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
return parent.DESCRIPTOR.fields_by_name[field_name].enum_type.values_by_number.get(field_value).name
|
||||||
|
|
||||||
|
|
||||||
|
def convert_secret_from_bytes_to_base32_str(bytes):
|
||||||
|
return str(base64.b32encode(bytes), 'utf-8').replace('=', '')
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
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_file(args, otp.url, 'qr/{}-{}{}.png'.format(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()
|
||||||
|
qr.add_data(data)
|
||||||
|
img = qr.make_image(fill_color='black', back_color='white')
|
||||||
|
if verbose: print('Saving to {}'.format(name))
|
||||||
|
img.save(name)
|
||||||
|
|
||||||
|
|
||||||
|
def print_qr(args, data):
|
||||||
|
from qrcode import QRCode
|
||||||
|
qr = QRCode()
|
||||||
|
qr.add_data(data)
|
||||||
|
qr.print_ascii()
|
||||||
|
|
||||||
|
|
||||||
def write_csv(args, otps):
|
def write_csv(args, otps):
|
||||||
global verbose, quiet
|
global verbose, quiet
|
||||||
if args.csv and len(otps) > 0:
|
if args.csv and len(otps) > 0:
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
wheel
|
|
||||||
4
requirements-dev.txt
Normal file
4
requirements-dev.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
wheel
|
||||||
|
pytest
|
||||||
|
flake8
|
||||||
|
pylint
|
||||||
89
test/print_verbose_output.txt
Normal file
89
test/print_verbose_output.txt
Normal 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
|
||||||
|
|
||||||
@@ -101,6 +101,19 @@ def test_extract_printqr(capsys):
|
|||||||
assert captured.err == ''
|
assert captured.err == ''
|
||||||
|
|
||||||
|
|
||||||
|
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 cleanup():
|
def cleanup():
|
||||||
remove_file('test_example_output.csv')
|
remove_file('test_example_output.csv')
|
||||||
remove_file('test_example_output.json')
|
remove_file('test_example_output.json')
|
||||||
|
|||||||
@@ -108,6 +108,16 @@ Type: OTP_TOTP
|
|||||||
|
|
||||||
self.assertEqual(actual_output, expected_output)
|
self.assertEqual(actual_output, expected_output)
|
||||||
|
|
||||||
|
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 setUp(self):
|
def setUp(self):
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user