mirror of
https://github.com/scito/extract_otp_secrets.git
synced 2025-12-14 19:01:03 +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]
|
||||
|
||||
@@ -30,6 +30,3 @@ jobs:
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest
|
||||
- name: Test with unittest
|
||||
run: |
|
||||
python -m unittest
|
||||
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,11 +1,15 @@
|
||||
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
|
||||
|
||||
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/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
|
||||
|
||||
@@ -26,6 +29,8 @@ Known to work with
|
||||
|
||||
* 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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### 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 +123,12 @@ 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
|
||||
|
||||
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
|
||||
|
||||
|
||||
# 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 sys_main():
|
||||
main(sys.argv[1:])
|
||||
|
||||
|
||||
def convert_secret_from_bytes_to_base32_str(bytes):
|
||||
return str(base64.b32encode(bytes), 'utf-8').replace('=', '')
|
||||
def main(sys_args):
|
||||
global verbose, quiet
|
||||
args = parse_args(sys_args)
|
||||
verbose = args.verbose
|
||||
quiet = args.quiet
|
||||
|
||||
|
||||
def save_qr(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()
|
||||
otps = extract_otps(args)
|
||||
write_csv(args, otps)
|
||||
write_json(args, otps)
|
||||
|
||||
|
||||
def parse_args(sys_args):
|
||||
@@ -96,21 +84,6 @@ def parse_args(sys_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):
|
||||
global verbose, quiet
|
||||
quiet = args.quiet
|
||||
@@ -121,7 +94,39 @@ def extract_otps(args):
|
||||
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))
|
||||
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):
|
||||
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:
|
||||
@@ -131,42 +136,63 @@ def extract_otps(args):
|
||||
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')
|
||||
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))
|
||||
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 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 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(args, otp_url, 'qr/{}-{}{}.png'.format(j, file_otp_name, '-' + file_otp_issuer if file_otp_issuer else ''))
|
||||
if not quiet: print()
|
||||
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
|
||||
|
||||
otps.append({
|
||||
"name": otp.name,
|
||||
"secret": secret,
|
||||
"issuer": otp.issuer,
|
||||
"type": otp_type,
|
||||
"url": otp_url
|
||||
})
|
||||
return otps
|
||||
|
||||
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):
|
||||
|
||||
@@ -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 == ''
|
||||
|
||||
|
||||
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():
|
||||
remove_file('test_example_output.csv')
|
||||
remove_file('test_example_output.json')
|
||||
|
||||
@@ -108,6 +108,16 @@ Type: OTP_TOTP
|
||||
|
||||
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):
|
||||
self.cleanup()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user