diff --git a/README.md b/README.md index 48b5393..45d8391 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,10 @@ Requires: git clone https://github.com/CooperRS/decrypt-otpauth-files.git cd decrypt-otpauth-files pipenv install -pipenv run python decrypt_otpauth.py --encrypted-otpauth-backup +# Decrypt a full backup file +pipenv run python decrypt_otpauth.py decrypt_backup --encrypted-otpauth-backup +# Decrypt a single account export +pipenv run python decrypt_otpauth.py decrypt_account --encrypted-otpauth-account ``` ## Demo diff --git a/decrypt_otpauth.py b/decrypt_otpauth.py index 1883551..3a920c3 100644 --- a/decrypt_otpauth.py +++ b/decrypt_otpauth.py @@ -4,7 +4,6 @@ import getpass import hashlib from enum import Enum -from itertools import chain from urllib.parse import quote import pyqrcode @@ -95,6 +94,32 @@ class OTPAccount: refDate = archive.decode("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({'NSMutableString': MutableString}) @@ -130,12 +155,62 @@ class DangerousUnarchive(archiver.Unarchive): return obj -@click.command() +def render_qr_to_terminal(otp_uri, type, issuer, label): + qr = pyqrcode.create(otp_uri, error="L") + click.echo("") + click.echo(f'{type}: {issuer} - {label}') + click.echo(qr.terminal(quiet_zone=4)) + click.echo("") + + +@click.group() +def cli(): + pass + + +@cli.command() +@click.option('--encrypted-otpauth-account', + 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}: ') + + # 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', help="path to your encrypted OTP Auth backup (.otpauthdb)", required=True, type=click.File('rb')) -def main(encrypted_otpauth_backup): +def decrypt_backup(encrypted_otpauth_backup): # Get password from user password = getpass.getpass(f'Password for export file {encrypted_otpauth_backup.name}: ') @@ -164,25 +239,9 @@ def main(encrypted_otpauth_backup): accounts = [account for folder in archive['Folders'] for account in folder.accounts] for account in accounts: - otp_type = account.type - otp_label = quote(f'{account.issuer}:{account.label}') - otp_parameters = { - 'secret': base64.b32encode(account.secret).decode("utf-8"), - 'algorithm': account.algorithm, - 'period': account.period, - 'digits': account.digits, - 'issuer': account.issuer, - 'counter': account.counter, - } - otp_parameters = '&'.join([f'{str(k)}={quote(str(v))}' for (k, v) in otp_parameters.items() if v]) - otp_uri = f'otpauth://{otp_type}/{otp_label}?{otp_parameters}' - qr = pyqrcode.create(otp_uri, error="L") - click.echo("") - click.echo(f'{account.type}: {account.issuer} - {account.label}') - click.echo(qr.terminal(quiet_zone=4)) - click.echo("") + render_qr_to_terminal(account.otp_uri(), account.type, account.issuer, account.label) input("Press Enter to continue...") if __name__ == '__main__': - main() + cli()