Update for modern Python and UV

This commit is contained in:
2025-07-20 16:29:03 +01:00
parent 2b71b60238
commit 977c50e227
6 changed files with 223 additions and 199 deletions

16
Pipfile
View File

@@ -1,16 +0,0 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[dev-packages]
[packages]
rncryptor = "==3.2.0"
bpylist = "==0.1.4"
click = "==6.7"
pyqrcode = "==1.2.1"
pycryptodome = "==3.9.1"
[requires]
python_version = "3.7"

90
Pipfile.lock generated
View File

@@ -1,90 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "3a9aa3cf92e33eb33f1ae837e0b466ec0827564e1abc6766112371ee29d937c3"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.python.org/simple",
"verify_ssl": true
}
]
},
"default": {
"bpylist": {
"hashes": [
"sha256:ac065c9f7b804a201999ebbc31b02df18e0fc29e03bcb1eac3e63696599b88ce"
],
"index": "pypi",
"version": "==0.1.4"
},
"click": {
"hashes": [
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
],
"index": "pypi",
"version": "==6.7"
},
"pycryptodome": {
"hashes": [
"sha256:0aa49f3fa110f8dc090bad1671a768cc17d3d3bd01566641ffc0d10d0fec8d49",
"sha256:0fafd3c4fb76c6992f34bf2d074f582f388e3b8062b8ba5d65b020634cc221e6",
"sha256:17eb9bd5d30a71b0c8a832e3e9cd2b7723f99907c38dc5dd23e59e8c368a70e2",
"sha256:2776255d5c748782f095ec422d42da2eadd8392ac9de7da23db4aed4231272bd",
"sha256:3500826dc3b9a8fdb762bebe551106081a6bdecd4181a3d1bd0206e48bba8974",
"sha256:3aa0d30326dcdef24c632d5c03b8e4d379c6ae0645082b27dd69ea816bb97ecb",
"sha256:3c7769bdadcc4809508e71997008912cc6d94fd7b5b1f3ef121683ebcac71d81",
"sha256:3e8c97a38dac6dafd180b4696a522b1581dd1a8e0ea60763458be547bac97361",
"sha256:5aca5125a46e458b308b5571ce8fe36d2229f161aa7db27b3ecacded70c6aa8b",
"sha256:62beb75f0688f406946312bfef8923d8ab23f5b8013acded931413625299d317",
"sha256:7725643de3c884a9945a086670787dce637037f32c5c2df7fd602bd5967f3486",
"sha256:872191a02a0c2a3b98dc75c62b32912b220a8ae5ff6ac9e39868f903f55dd6a4",
"sha256:8c501e80960d12328d49e1d409daf426f29364a37c602f257c99509999654650",
"sha256:9512638bfef8ffc94c62751965a4733c3792104dc84771ba54ce0f80f49134df",
"sha256:962043051afa7a5ab071b0d8996dc00e564327a18566d3e574a39cb6e097b462",
"sha256:9db72b18b30902a83fa57b0d7dae4ce24f85186695e3bea0d423f1ec7c5b3fbe",
"sha256:9ffd4f0bfb5949dfa0e5cedef836364f18da0deb2fba04671607fb3b59b29112",
"sha256:a26819f693cf5fc0a2373a3e4b91c38e359cad9f00020a885b667c77f28738d5",
"sha256:a3efc575a53511c48361d933e12e07c2eb940db1afda0995285176c372ab7352",
"sha256:ababd6685b9d94729a851a0615482156afdacbeaabeea60f67961db0e975b1af",
"sha256:b0e9c8c270cd3f8c73b53139f0708f257189a00bbc898be6d3f03995e5f7edc2",
"sha256:b74173b13c221ee96b608212b9adc2c459a73d3632f04490df42e4f07e7041e6",
"sha256:bed297f75ba19cefe2d10beb4959f4f8cb62c2560a3998ad87479485098ee939",
"sha256:c639f09e8ce8ad5af9884233f952ade4b73a11b7d41d3b9bb7d4e64d9e1df164",
"sha256:c7bc308be67288af1cd44668d59e36356f0ce518337899079ddb0235bd55db79",
"sha256:cca152dcebc318833ba70499190ce17ee81b525404e2a7548c77f52b439306a7",
"sha256:d5261d22bc3a54db26f11dabcda14bbaab72080977e083d795b4b1d1b510c774",
"sha256:d81111e3da7fc9eee825ba7d8a68b3c1464f41110ef98a7280e0c7fb82c91e73",
"sha256:d95fafa899abb9f82e55ff43f423e100784312b43932514f2c05d41cbb20323e",
"sha256:de411a64d4105d4424441833bd25943208e58c846abf981bba5bbeeba88a49c3",
"sha256:e02c7b3d05b88ff1a236e49a252b2bf8444d3a1d04a056784af766c0909eba36",
"sha256:fbafe9b01b717e0bfbc83cd740ff5bf5cdd3f208815be470ea203942b899bbdf"
],
"index": "pypi",
"version": "==3.9.1"
},
"pyqrcode": {
"hashes": [
"sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6",
"sha256:fdbf7634733e56b72e27f9bce46e4550b75a3a2c420414035cae9d9d26b234d5"
],
"index": "pypi",
"version": "==1.2.1"
},
"rncryptor": {
"hashes": [
"sha256:156253246f3e3521e5080191e9b4ec100e162d07c261a9df86169f8943bcc7a3",
"sha256:a3b521d22953bffc4f125faf53f4c9c2a41c8186e0b0e331314abe455be92c48"
],
"index": "pypi",
"version": "==3.2.0"
}
},
"develop": {}
}

View File

@@ -4,10 +4,12 @@ This tool allows for decrypting the encrypted backups/account files created by [
If you find problems with the file format (in particular security related issues), do not hesitate and file an issue. If you find problems with the file format (in particular security related issues), do not hesitate and file an issue.
This is a fork of the original repository at [CooperRS/decrypt-otpauth-files](https://github.com/CooperRS/decrypt-otpauth-files) with a aim to get the script working out of the box with modern Python versions.
## Requirements ## Requirements
- [Python 3.7](https://www.python.org/downloads/) - [Python 3.12](https://www.python.org/downloads/)
- [pipenv](https://github.com/pypa/pipenv) - [uv](https://astral.sh/uv)
- An encrypted OTP Auth backup/account file - An encrypted OTP Auth backup/account file
## Usage ## Usage
@@ -21,20 +23,20 @@ cd decrypt-otpauth-files
2. Install dependencies 2. Install dependencies
``` ```shell-session
pipenv install $ uv sync
``` ```
3. Decrypt your OTP Auth file 3. Decrypt your OTP Auth file
``` ```shell-session
# Decrypt a full backup file # Decrypt a full backup file
pipenv run python decrypt_otpauth.py decrypt_backup --encrypted-otpauth-backup <path to your OTP Auth backup> $ uv run python decrypt_otpauth.py decrypt_backup --encrypted-otpauth-backup <path to your OTP Auth backup>
``` ```
``` ```shell-session
# Decrypt a single account export # Decrypt a single account export
pipenv run python decrypt_otpauth.py decrypt_account --encrypted-otpauth-account <path to your OTP Auth account> $ uv run python decrypt_otpauth.py decrypt_account --encrypted-otpauth-account <path to your OTP Auth account>
``` ```
## Demo ## Demo

182
decrypt_otpauth.py Normal file → Executable file
View File

@@ -1,20 +1,16 @@
#!/usr/bin/env python3
import base64 import base64
import click
import getpass import getpass
import hashlib import hashlib
import plistlib
from enum import Enum from enum import Enum
from urllib.parse import quote from urllib.parse import quote
import click
import pyqrcode import pyqrcode
from bpylist2 import archiver
from bpylist import archiver
from bpylist.archive_types import uid
from Crypto.Cipher import AES from Crypto.Cipher import AES
from rncryptor import RNCryptor, bord
from rncryptor import RNCryptor
from rncryptor import bord
class Type(Enum): class Type(Enum):
@@ -25,11 +21,11 @@ class Type(Enum):
@property @property
def uri_value(self): def uri_value(self):
if self.value == 0: if self.value == 0:
return 'unknown' return "unknown"
if self.value == 1: if self.value == 1:
return 'hotp' return "hotp"
if self.value == 2: if self.value == 2:
return 'totp' return "totp"
class Algorithm(Enum): class Algorithm(Enum):
@@ -42,27 +38,25 @@ class Algorithm(Enum):
@property @property
def uri_value(self): def uri_value(self):
if self.value == 0: if self.value == 0:
return 'sha1' return "sha1"
if self.value == 1: if self.value == 1:
return 'sha1' return "sha1"
if self.value == 2: if self.value == 2:
return 'sha256' return "sha256"
if self.value == 3: if self.value == 3:
return 'sha512' return "sha512"
if self.value == 4: if self.value == 4:
return 'md5' return "md5"
class MutableString: class MutableString:
def decode_archive(archive): def decode_archive(archive):
return archive.decode('NS.string') return archive.decode("NS.string")
class MutableData: class MutableData:
def decode_archive(archive): def decode_archive(archive):
return bytes(archive.decode('NS.data')) return bytes(archive.decode("NS.data"))
class OTPFolder: class OTPFolder:
@@ -74,11 +68,11 @@ class OTPFolder:
self.accounts = accounts self.accounts = accounts
def __repr__(self): def __repr__(self):
return f'<OTPFolder: {self.name}>' return f"<OTPFolder: {self.name}>"
def decode_archive(archive): def decode_archive(archive):
name = archive.decode('name') name = archive.decode("name")
accounts = archive.decode('accounts') accounts = archive.decode("accounts")
return OTPFolder(name, accounts) return OTPFolder(name, accounts)
@@ -93,7 +87,9 @@ class OTPAccount:
period = None period = None
refDate = None refDate = None
def __init__(self, label, issuer, secret, type, algorithm, digits, counter, period, refDate): def __init__(
self, label, issuer, secret, type, algorithm, digits, counter, period, refDate
):
self.label = label self.label = label
self.issuer = issuer self.issuer = issuer
self.secret = secret self.secret = secret
@@ -105,7 +101,7 @@ class OTPAccount:
self.refDate = refDate self.refDate = refDate
def __repr__(self): def __repr__(self):
return f'<OTPAccount: {self.issuer} ({self.label})>' return f"<OTPAccount: {self.issuer} ({self.label})>"
def decode_archive(archive): def decode_archive(archive):
label = archive.decode("label") label = archive.decode("label")
@@ -117,7 +113,9 @@ class OTPAccount:
counter = archive.decode("counter") counter = archive.decode("counter")
period = archive.decode("period") period = archive.decode("period")
refDate = archive.decode("refDate") refDate = archive.decode("refDate")
return OTPAccount(label, issuer, secret, type, algorithm, digits, counter, period, refDate) return OTPAccount(
label, issuer, secret, type, algorithm, digits, counter, period, refDate
)
def from_dict(in_dict): def from_dict(in_dict):
label = in_dict.get("label") label = in_dict.get("label")
@@ -129,40 +127,42 @@ class OTPAccount:
counter = in_dict.get("counter") counter = in_dict.get("counter")
period = in_dict.get("period") period = in_dict.get("period")
refDate = in_dict.get("refDate") refDate = in_dict.get("refDate")
return OTPAccount(label, issuer, secret, type, algorithm, digits, counter, period, refDate) return OTPAccount(
label, issuer, secret, type, algorithm, digits, counter, period, refDate
)
def otp_uri(self): def otp_uri(self):
otp_type = self.type.uri_value otp_type = self.type.uri_value
otp_label = quote(f'{self.issuer}:{self.label}') otp_label = quote(f"{self.issuer}:{self.label}")
otp_parameters = { otp_parameters = {
'secret': base64.b32encode(self.secret).decode("utf-8").rstrip("="), "secret": base64.b32encode(self.secret).decode("utf-8").rstrip("="),
'algorithm': self.algorithm.uri_value, "algorithm": self.algorithm.uri_value,
'period': self.period, "period": self.period,
'digits': self.digits, "digits": self.digits,
'issuer': self.issuer, "issuer": self.issuer,
'counter': self.counter, "counter": self.counter,
} }
otp_parameters = '&'.join([f'{str(k)}={quote(str(v))}' for (k, v) in otp_parameters.items() if v]) otp_parameters = "&".join(
return f'otpauth://{otp_type}/{otp_label}?{otp_parameters}' [f"{str(k)}={quote(str(v))}" for (k, v) in otp_parameters.items() if v]
)
return f"otpauth://{otp_type}/{otp_label}?{otp_parameters}"
archiver.update_class_map({'NSMutableData': MutableData}) archiver.update_class_map({"NSMutableData": MutableData})
archiver.update_class_map({'NSMutableString': MutableString}) archiver.update_class_map({"NSMutableString": MutableString})
archiver.update_class_map({'ACOTPFolder': OTPFolder}) archiver.update_class_map({"ACOTPFolder": OTPFolder})
archiver.update_class_map({'ACOTPAccount': OTPAccount}) archiver.update_class_map({"ACOTPAccount": OTPAccount})
class RawRNCryptor(RNCryptor): class RawRNCryptor(RNCryptor):
def post_decrypt_data(self, data): def post_decrypt_data(self, data):
"""Remove useless symbols which """Remove useless symbols which
appear over padding for AES (PKCS#7).""" appear over padding for AES (PKCS#7)."""
data = data[:-bord(data[-1])] data = data[: -bord(data[-1])]
return data return data
class DangerousUnarchive(archiver.Unarchive): class DangerousUnarchive(archiver.Unarchive):
def decode_object(self, index): def decode_object(self, index):
if index == 0: if index == 0:
return None return None
@@ -178,8 +178,8 @@ class DangerousUnarchive(archiver.Unarchive):
if not isinstance(raw_obj, dict): if not isinstance(raw_obj, dict):
return raw_obj return raw_obj
class_uid = raw_obj.get('$class') class_uid = raw_obj.get("$class")
if not isinstance(class_uid, uid): if not isinstance(class_uid, plistlib.UID):
raise archiver.MissingClassUID(raw_obj) raise archiver.MissingClassUID(raw_obj)
klass = self.class_for_uid(class_uid) klass = self.class_for_uid(class_uid)
@@ -192,7 +192,7 @@ class DangerousUnarchive(archiver.Unarchive):
def render_qr_to_terminal(otp_uri, type, issuer, label): def render_qr_to_terminal(otp_uri, type, issuer, label):
qr = pyqrcode.create(otp_uri, error="L") qr = pyqrcode.create(otp_uri, error="L")
click.echo("") click.echo("")
click.echo(f'{type}: {issuer} - {label}') click.echo(f"{type}: {issuer} - {label}")
click.echo(qr.terminal(quiet_zone=4)) click.echo(qr.terminal(quiet_zone=4))
click.echo("") click.echo("")
@@ -203,45 +203,51 @@ def cli():
@cli.command() @cli.command()
@click.option('--encrypted-otpauth-account', @click.option(
help="path to your encrypted OTP Auth account (.otpauth)", "--encrypted-otpauth-account",
required=True, help="path to your encrypted OTP Auth account (.otpauth)",
type=click.File('rb')) required=True,
type=click.File("rb"),
)
def decrypt_account(encrypted_otpauth_account): def decrypt_account(encrypted_otpauth_account):
# Get password from user # Get password from user
password = getpass.getpass(f'Password for export file {encrypted_otpauth_account.name}: ') password = getpass.getpass(
f"Password for export file {encrypted_otpauth_account.name}: "
)
# Get IV and key for wrapping archive # Get IV and key for wrapping archive
iv = bytes(16) iv = bytes(16)
key = hashlib.sha256('OTPAuth'.encode('utf-8')).digest() key = hashlib.sha256("OTPAuth".encode("utf-8")).digest()
# Decrypt wrapping archive # Decrypt wrapping archive
data = AES.new(key, AES.MODE_CBC, iv).decrypt(encrypted_otpauth_account.read()) data = AES.new(key, AES.MODE_CBC, iv).decrypt(encrypted_otpauth_account.read())
data = data[:-data[-1]] data = data[: -data[-1]]
# Decode wrapping archive # Decode wrapping archive
archive = archiver.Unarchive(data).top_object() archive = archiver.Unarchive(data).top_object()
if archive['Version'] == 1.1: if archive["Version"] == 1.1:
account = decrypt_account_11(archive, password) account = decrypt_account_11(archive, password)
elif archive['Version'] == 1.2: elif archive["Version"] == 1.2:
account = decrypt_account_12(archive, password) account = decrypt_account_12(archive, password)
else: else:
click.echo(f'Encountered unknow file version: {archive["Version"]}') click.echo(f"Encountered unknow file version: {archive['Version']}")
return return
render_qr_to_terminal(account.otp_uri(), account.type, account.issuer, account.label) render_qr_to_terminal(
account.otp_uri(), account.type, account.issuer, account.label
)
def decrypt_account_11(archive, password): def decrypt_account_11(archive, password):
# Get IV and key for actual archive # Get IV and key for actual archive
iv = hashlib.sha1(archive['IV']).digest()[:16] iv = hashlib.sha1(archive["IV"]).digest()[:16]
salt = archive['Salt'] salt = archive["Salt"]
key = hashlib.sha256((salt + '-' + password).encode('utf-8')).digest() key = hashlib.sha256((salt + "-" + password).encode("utf-8")).digest()
# Decrypt actual archive # Decrypt actual archive
data = AES.new(key, AES.MODE_CBC, iv).decrypt(archive['Data']) data = AES.new(key, AES.MODE_CBC, iv).decrypt(archive["Data"])
data = data[:-data[-1]] data = data[: -data[-1]]
# Decode actual archive # Decode actual archive
archive = DangerousUnarchive(data).top_object() archive = DangerousUnarchive(data).top_object()
@@ -252,7 +258,7 @@ def decrypt_account_11(archive, password):
def decrypt_account_12(archive, password): def decrypt_account_12(archive, password):
# Decrypt using RNCryptor # Decrypt using RNCryptor
data = data = RawRNCryptor().decrypt(archive['Data'], password) data = data = RawRNCryptor().decrypt(archive["Data"], password)
# Decode archive # Decode archive
archive = DangerousUnarchive(data).top_object() archive = DangerousUnarchive(data).top_object()
@@ -262,63 +268,69 @@ def decrypt_account_12(archive, password):
@cli.command() @cli.command()
@click.option('--encrypted-otpauth-backup', @click.option(
help="path to your encrypted OTP Auth backup (.otpauthdb)", "--encrypted-otpauth-backup",
required=True, help="path to your encrypted OTP Auth backup (.otpauthdb)",
type=click.File('rb')) required=True,
type=click.File("rb"),
)
def decrypt_backup(encrypted_otpauth_backup): def decrypt_backup(encrypted_otpauth_backup):
# Get password from user # Get password from user
password = getpass.getpass(f'Password for export file {encrypted_otpauth_backup.name}: ') password = getpass.getpass(
f"Password for export file {encrypted_otpauth_backup.name}: "
)
# Get IV and key for wrapping archive # Get IV and key for wrapping archive
iv = bytes(16) iv = bytes(16)
key = hashlib.sha256('Authenticator'.encode('utf-8')).digest() key = hashlib.sha256("Authenticator".encode("utf-8")).digest()
# Decrypt wrapping archive # Decrypt wrapping archive
data = AES.new(key, AES.MODE_CBC, iv).decrypt(encrypted_otpauth_backup.read()) data = AES.new(key, AES.MODE_CBC, iv).decrypt(encrypted_otpauth_backup.read())
data = data[:-data[-1]] data = data[: -data[-1]]
# Decode wrapping archive # Decode wrapping archive
archive = archiver.Unarchive(data).top_object() archive = archiver.Unarchive(data).top_object()
if archive['Version'] == 1.0: if archive["Version"] == 1.0:
accounts = decrypt_backup_10(archive, password) accounts = decrypt_backup_10(archive, password)
elif archive['Version'] == 1.1: elif archive["Version"] == 1.1:
accounts = decrypt_backup_11(archive, password) accounts = decrypt_backup_11(archive, password)
else: else:
click.echo(f'Encountered unknow file version: {archive["Version"]}') click.echo(f"Encountered unknow file version: {archive['Version']}")
return return
for account in accounts: for account in accounts:
render_qr_to_terminal(account.otp_uri(), account.type, account.issuer, account.label) render_qr_to_terminal(
account.otp_uri(), account.type, account.issuer, account.label
)
input("Press Enter to continue...") input("Press Enter to continue...")
def decrypt_backup_10(archive, password): def decrypt_backup_10(archive, password):
# Get IV and key for actual archive # Get IV and key for actual archive
iv = hashlib.sha1(archive['IV'].encode('utf-8')).digest()[:16] iv = hashlib.sha1(archive["IV"].encode("utf-8")).digest()[:16]
salt = archive['Salt'] salt = archive["Salt"]
key = hashlib.sha256((salt + '-' + password).encode('utf-8')).digest() key = hashlib.sha256((salt + "-" + password).encode("utf-8")).digest()
# Decrypt actual archive # Decrypt actual archive
data = AES.new(key, AES.MODE_CBC, iv).decrypt(archive['WrappedData']) data = AES.new(key, AES.MODE_CBC, iv).decrypt(archive["WrappedData"])
data = data[:-data[-1]] data = data[: -data[-1]]
# Decode actual archive # Decode actual archive
archive = DangerousUnarchive(data).top_object() archive = DangerousUnarchive(data).top_object()
return [account for folder in archive['Folders'] for account in folder.accounts] return [account for folder in archive["Folders"] for account in folder.accounts]
def decrypt_backup_11(archive, password): def decrypt_backup_11(archive, password):
# Decrypt using RNCryptor # Decrypt using RNCryptor
data = data = RawRNCryptor().decrypt(archive['WrappedData'], password) data = data = RawRNCryptor().decrypt(archive["WrappedData"], password)
# Decode archive # Decode archive
archive = DangerousUnarchive(data).top_object() archive = DangerousUnarchive(data).top_object()
return [account for folder in archive['Folders'] for account in folder.accounts] return [account for folder in archive["Folders"] for account in folder.accounts]
if __name__ == '__main__': if __name__ == "__main__":
cli() cli()

14
pyproject.toml Normal file
View File

@@ -0,0 +1,14 @@
[project]
name = "decrypt-otpauth-files"
version = "1.0.0"
description = "A tool for decrypting the encrypted backups/account files created by OTP Auth for iOS"
license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"bpylist2>=4.1.1",
"click>=8.2.1",
"pycryptodome>=3.23.0",
"pyqrcode>=1.2.1",
"rncryptor>=3.3.0",
]

102
uv.lock generated Normal file
View File

@@ -0,0 +1,102 @@
version = 1
revision = 2
requires-python = ">=3.12"
[[package]]
name = "bpylist2"
version = "4.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/34/eb90ff6be953f6e4df08d4e8c0b761bea144242b6d711e922113411cc631/bpylist2-4.1.1.tar.gz", hash = "sha256:0cc63284aee42f5c7e0ec87f8f59cdd35aaed05ad12d866b1868ea0c0caaafe1", size = 18000, upload-time = "2023-09-11T16:58:00.758Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/e5/b552c94fc965153c9dcad4b64202e5170d4918716bc082d4fa6c6230579c/bpylist2-4.1.1-py3-none-any.whl", hash = "sha256:4862eab78d9d908d532393208b6771cebc8debef99ab851b54a0a0e28e2bec6b", size = 16596, upload-time = "2023-09-11T16:57:59.526Z" },
]
[[package]]
name = "click"
version = "8.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "decrypt-otpauth-files"
version = "1.0.0"
source = { virtual = "." }
dependencies = [
{ name = "bpylist2" },
{ name = "click" },
{ name = "pycryptodome" },
{ name = "pyqrcode" },
{ name = "rncryptor" },
]
[package.metadata]
requires-dist = [
{ name = "bpylist2", specifier = ">=4.1.1" },
{ name = "click", specifier = ">=8.2.1" },
{ name = "pycryptodome", specifier = ">=3.23.0" },
{ name = "pyqrcode", specifier = ">=1.2.1" },
{ name = "rncryptor", specifier = ">=3.3.0" },
]
[[package]]
name = "pycryptodome"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" },
{ url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" },
{ url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" },
{ url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" },
{ url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" },
{ url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" },
{ url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" },
{ url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" },
{ url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" },
{ url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" },
{ url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" },
{ url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
{ url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
{ url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
{ url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
{ url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
{ url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
{ url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
{ url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
{ url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
{ url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
{ url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
]
[[package]]
name = "pyqrcode"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/37/61/f07226075c347897937d4086ef8e55f0a62ae535e28069884ac68d979316/PyQRCode-1.2.1.tar.gz", hash = "sha256:fdbf7634733e56b72e27f9bce46e4550b75a3a2c420414035cae9d9d26b234d5", size = 36989, upload-time = "2016-06-20T03:28:03.411Z" }
[[package]]
name = "rncryptor"
version = "3.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycryptodome" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/f0/a6b66f0a13bf622231613023fd4682a6abf480fde48c058c7b218d9b635a/rncryptor-3.3.0.tar.gz", hash = "sha256:efdd61d56627e645dcbb1b9cfe22806decf676c0089b6f993f280de82d66e70e", size = 4129, upload-time = "2019-08-14T16:49:09.398Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/57/0734b54bd4a48df3dbd3aa0ac86fd24296bc22e3e9aa90961eab39fb223d/rncryptor-3.3.0-py2.py3-none-any.whl", hash = "sha256:ba932299aee25524537a32112cba8787965f8de6e46550684b71a3c1255b0e62", size = 4264, upload-time = "2019-08-14T16:49:07.864Z" },
]