mirror of
https://github.com/nikdoof/decrypt-otpauth-files.git
synced 2025-12-17 19:49:37 +00:00
Merge pull request #1 from ewdurbin/master
Render QR Codes to terminal!
This commit is contained in:
2
Pipfile
2
Pipfile
@@ -13,6 +13,8 @@ name = "pypi"
|
|||||||
|
|
||||||
bpylist = "==0.1.4"
|
bpylist = "==0.1.4"
|
||||||
click = "==6.7"
|
click = "==6.7"
|
||||||
|
pycrypto = "==2.6.1"
|
||||||
|
pyqrcode = "==1.2.1"
|
||||||
|
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
|
|||||||
15
Pipfile.lock
generated
15
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "f412a27aca9ac43baaa70aad1e56e911c64dc2c33b5fac3f039380103c38d197"
|
"sha256": "81d534465fc2e9af42dacab3d37d305414bfe0fb1642f22fb3142ffebf745505"
|
||||||
},
|
},
|
||||||
"host-environment-markers": {
|
"host-environment-markers": {
|
||||||
"implementation_name": "cpython",
|
"implementation_name": "cpython",
|
||||||
@@ -41,6 +41,19 @@
|
|||||||
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
|
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
|
||||||
],
|
],
|
||||||
"version": "==6.7"
|
"version": "==6.7"
|
||||||
|
},
|
||||||
|
"pycrypto": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:f2ce1e989b272cfcb677616763e0a2e7ec659effa67a88aa92b3a65528f60a3c"
|
||||||
|
],
|
||||||
|
"version": "==2.6.1"
|
||||||
|
},
|
||||||
|
"pyqrcode": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:fdbf7634733e56b72e27f9bce46e4550b75a3a2c420414035cae9d9d26b234d5",
|
||||||
|
"sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6"
|
||||||
|
],
|
||||||
|
"version": "==1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {}
|
"develop": {}
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ Requires:
|
|||||||
git clone https://github.com/CooperRS/decrypt-otpauth-files.git
|
git clone https://github.com/CooperRS/decrypt-otpauth-files.git
|
||||||
cd decrypt-otpauth-files
|
cd decrypt-otpauth-files
|
||||||
pipenv install
|
pipenv install
|
||||||
pipenv run python decrypt_otpauth.py --encrypted-otpauth-backup <path to your OTP Auth backup>
|
# Decrypt a full backup file
|
||||||
|
pipenv run python decrypt_otpauth.py decrypt_backup --encrypted-otpauth-backup <path to your OTP Auth backup>
|
||||||
|
# Decrypt a single account export
|
||||||
|
pipenv run python decrypt_otpauth.py decrypt_account --encrypted-otpauth-account <path to your OTP Auth account>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
@@ -28,6 +31,8 @@ The project contains two OTP Auth exports for demo purposes:
|
|||||||
|
|
||||||
The password for both files is `abc123`.
|
The password for both files is `abc123`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
Inspired by [ewdurbin](https://github.com/ewdurbin) and his [evacuate_2STP](https://github.com/ewdurbin/evacuate_2stp) repo.
|
Inspired by [ewdurbin](https://github.com/ewdurbin) and his [evacuate_2STP](https://github.com/ewdurbin/evacuate_2stp) repo.
|
||||||
|
|||||||
@@ -1,36 +1,45 @@
|
|||||||
|
import base64
|
||||||
import click
|
import click
|
||||||
import getpass
|
import getpass
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from Crypto.Cipher import AES
|
import pyqrcode
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from bpylist import archiver
|
from bpylist import archiver
|
||||||
from bpylist.archive_types import uid
|
from bpylist.archive_types import uid
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
|
||||||
|
|
||||||
class Type(Enum):
|
class Type(Enum):
|
||||||
Unknown = 0
|
Unknown = 0
|
||||||
HOTP = 1
|
HOTP = 1
|
||||||
TOTP = 2
|
TOTP = 2
|
||||||
|
|
||||||
|
|
||||||
class Algorithm(Enum):
|
class Algorithm(Enum):
|
||||||
Unknown = 0
|
Unknown = 0
|
||||||
SHA1 = 1 # Used in case of Unknown
|
SHA1 = 1 # Used in case of Unknown
|
||||||
SHA256 = 2
|
SHA256 = 2
|
||||||
SHA512 = 3
|
SHA512 = 3
|
||||||
MD5 = 4
|
MD5 = 4
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
name = None
|
name = None
|
||||||
accounts = None
|
accounts = None
|
||||||
@@ -40,16 +49,17 @@ class OTPFolder:
|
|||||||
self.accounts = accounts
|
self.accounts = accounts
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<OTPFolder: {self.name}, {self.accounts}>'
|
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)
|
||||||
|
|
||||||
|
|
||||||
class OTPAccount:
|
class OTPAccount:
|
||||||
label = None
|
label = None
|
||||||
issue = None
|
issuer = None
|
||||||
secret = None
|
secret = None
|
||||||
type = None
|
type = None
|
||||||
algorithm = None
|
algorithm = None
|
||||||
@@ -70,7 +80,7 @@ class OTPAccount:
|
|||||||
self.refDate = refDate
|
self.refDate = refDate
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<OTPAccount: {self.label} ({self.issuer}), Secret: 0x{self.secret.hex()}, Type: {self.type}, Algorithm: {self.algorithm}, Digits: {self.digits}, Counter: {self.counter}, Period: {self.period}, Reference Date: {self.refDate}>'
|
return f'<OTPAccount: {self.issuer} ({self.label})>'
|
||||||
|
|
||||||
def decode_archive(archive):
|
def decode_archive(archive):
|
||||||
label = archive.decode("label")
|
label = archive.decode("label")
|
||||||
@@ -84,11 +94,39 @@ class OTPAccount:
|
|||||||
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):
|
||||||
|
label = in_dict.get("label")
|
||||||
|
issuer = in_dict.get("issuer")
|
||||||
|
secret = bytes(in_dict.get("secret"))
|
||||||
|
type = Type(in_dict.get("type"))
|
||||||
|
algorithm = Algorithm(in_dict.get("algorithm"))
|
||||||
|
digits = in_dict.get("digits")
|
||||||
|
counter = in_dict.get("counter")
|
||||||
|
period = in_dict.get("period")
|
||||||
|
refDate = in_dict.get("refDate")
|
||||||
|
return OTPAccount(label, issuer, secret, type, algorithm, digits, counter, period, refDate)
|
||||||
|
|
||||||
|
def otp_uri(self):
|
||||||
|
otp_type = self.type
|
||||||
|
otp_label = quote(f'{self.issuer}:{self.label}')
|
||||||
|
otp_parameters = {
|
||||||
|
'secret': base64.b32encode(self.secret).decode("utf-8"),
|
||||||
|
'algorithm': self.algorithm,
|
||||||
|
'period': self.period,
|
||||||
|
'digits': self.digits,
|
||||||
|
'issuer': self.issuer,
|
||||||
|
'counter': self.counter,
|
||||||
|
}
|
||||||
|
otp_parameters = '&'.join([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 DangerousUnarchive(archiver.Unarchive):
|
class DangerousUnarchive(archiver.Unarchive):
|
||||||
|
|
||||||
def decode_object(self, index):
|
def decode_object(self, index):
|
||||||
@@ -116,45 +154,63 @@ class DangerousUnarchive(archiver.Unarchive):
|
|||||||
self.unpacked_uids[index] = obj
|
self.unpacked_uids[index] = obj
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
# @click.command()
|
|
||||||
# @click.option('--encrypted-otpauth-account',
|
|
||||||
# help="path to your encrypted OTP Auth account (.otpauth)",
|
|
||||||
# required=True,
|
|
||||||
# type=click.File('rb'))
|
|
||||||
# def main(encrypted_otpauth_account):
|
|
||||||
# # Get password from user
|
|
||||||
# password = getpass.getpass(f'Password for export file {encrypted_otpauth_account.name}: ')
|
|
||||||
|
|
||||||
# # Get IV and key for wrapping archive
|
def render_qr_to_terminal(otp_uri, type, issuer, label):
|
||||||
# iv = bytes(16)
|
qr = pyqrcode.create(otp_uri, error="L")
|
||||||
# key = hashlib.sha256('OTPAuth'.encode('utf-8')).digest()
|
click.echo("")
|
||||||
|
click.echo(f'{type}: {issuer} - {label}')
|
||||||
|
click.echo(qr.terminal(quiet_zone=4))
|
||||||
|
click.echo("")
|
||||||
|
|
||||||
# # Decrypt wrapping archive
|
|
||||||
# data = AES.new(key, AES.MODE_CBC, iv).decrypt(encrypted_otpauth_account.read())
|
|
||||||
# data = data[:-data[-1]]
|
|
||||||
|
|
||||||
# # Decode wrapping archive
|
@click.group()
|
||||||
# archive = DangerousUnarchive(data).top_object()
|
def cli():
|
||||||
|
pass
|
||||||
|
|
||||||
# # Get IV and key for actual archive
|
|
||||||
# iv = hashlib.sha1(archive['IV']).digest()[:16]
|
|
||||||
# salt = archive['Salt']
|
|
||||||
# key = hashlib.sha256((salt + '-' + password).encode('utf-8')).digest()
|
|
||||||
|
|
||||||
# # Decrypt actual archive
|
|
||||||
# data = AES.new(key, AES.MODE_CBC, iv).decrypt(archive['Data'])
|
|
||||||
# data = data[:-data[-1]]
|
|
||||||
|
|
||||||
# # Decode actual archive
|
@cli.command()
|
||||||
# archive = DangerousUnarchive(data).top_object()
|
@click.option('--encrypted-otpauth-account',
|
||||||
# print(archive)
|
help="path to your encrypted OTP Auth account (.otpauth)",
|
||||||
|
required=True,
|
||||||
|
type=click.File('rb'))
|
||||||
|
def decrypt_account(encrypted_otpauth_account):
|
||||||
|
# Get password from user
|
||||||
|
password = getpass.getpass(f'Password for export file {encrypted_otpauth_account.name}: ')
|
||||||
|
|
||||||
@click.command()
|
# Get IV and key for wrapping archive
|
||||||
|
iv = bytes(16)
|
||||||
|
key = hashlib.sha256('OTPAuth'.encode('utf-8')).digest()
|
||||||
|
|
||||||
|
# Decrypt wrapping archive
|
||||||
|
data = AES.new(key, AES.MODE_CBC, iv).decrypt(encrypted_otpauth_account.read())
|
||||||
|
data = data[:-data[-1]]
|
||||||
|
|
||||||
|
# Decode wrapping archive
|
||||||
|
archive = archiver.Unarchive(data).top_object()
|
||||||
|
|
||||||
|
# Get IV and key for actual archive
|
||||||
|
iv = hashlib.sha1(archive['IV']).digest()[:16]
|
||||||
|
salt = archive['Salt']
|
||||||
|
key = hashlib.sha256((salt + '-' + password).encode('utf-8')).digest()
|
||||||
|
|
||||||
|
# Decrypt actual archive
|
||||||
|
data = AES.new(key, AES.MODE_CBC, iv).decrypt(archive['Data'])
|
||||||
|
data = data[:-data[-1]]
|
||||||
|
|
||||||
|
# Decode actual archive
|
||||||
|
archive = DangerousUnarchive(data).top_object()
|
||||||
|
|
||||||
|
# Construct OTPAccount object from returned dictionary
|
||||||
|
account = OTPAccount.from_dict(archive)
|
||||||
|
render_qr_to_terminal(account.otp_uri(), account.type, account.issuer, account.label)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
@click.option('--encrypted-otpauth-backup',
|
@click.option('--encrypted-otpauth-backup',
|
||||||
help="path to your encrypted OTP Auth backup (.otpauthdb)",
|
help="path to your encrypted OTP Auth backup (.otpauthdb)",
|
||||||
required=True,
|
required=True,
|
||||||
type=click.File('rb'))
|
type=click.File('rb'))
|
||||||
def main(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}: ')
|
||||||
|
|
||||||
@@ -167,21 +223,25 @@ def main(encrypted_otpauth_backup):
|
|||||||
data = data[:-data[-1]]
|
data = data[:-data[-1]]
|
||||||
|
|
||||||
# Decode wrapping archive
|
# Decode wrapping archive
|
||||||
archive = DangerousUnarchive(data).top_object()
|
archive = archiver.Unarchive(data).top_object()
|
||||||
|
|
||||||
# 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()
|
||||||
print(archive)
|
|
||||||
|
accounts = [account for folder in archive['Folders'] for account in folder.accounts]
|
||||||
|
for account in accounts:
|
||||||
|
render_qr_to_terminal(account.otp_uri(), account.type, account.issuer, account.label)
|
||||||
|
input("Press Enter to continue...")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
cli()
|
||||||
|
|||||||
Reference in New Issue
Block a user