Merge pull request #1 from ewdurbin/master

Render QR Codes to terminal!
This commit is contained in:
Roland Moers
2017-12-24 15:25:45 +01:00
committed by GitHub
5 changed files with 128 additions and 48 deletions

View File

@@ -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
View File

@@ -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": {}

View File

@@ -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`.
![example gif](demo.gif)
## 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.

View File

@@ -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()

BIN
demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 KiB