diff --git a/Pipfile b/Pipfile index 9aaf126..b71e7e0 100644 --- a/Pipfile +++ b/Pipfile @@ -11,6 +11,7 @@ name = "pypi" [packages] +rncryptor = "==3.2.0" bpylist = "==0.1.4" click = "==6.7" pycrypto = "==2.6.1" diff --git a/Pipfile.lock b/Pipfile.lock index 5f223ce..51b0ce6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "81d534465fc2e9af42dacab3d37d305414bfe0fb1642f22fb3142ffebf745505" + "sha256": "6b165af75f2543e65ceee44608b5377627ca18809ea66b2b85c012dcdcb9eeb8" }, "host-environment-markers": { "implementation_name": "cpython", @@ -54,6 +54,13 @@ "sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6" ], "version": "==1.2.1" + }, + "rncryptor": { + "hashes": [ + "sha256:a3b521d22953bffc4f125faf53f4c9c2a41c8186e0b0e331314abe455be92c48", + "sha256:156253246f3e3521e5080191e9b4ec100e162d07c261a9df86169f8943bcc7a3" + ], + "version": "==3.2.0" } }, "develop": {} diff --git a/test.otpauth b/account-1.1.otpauth similarity index 100% rename from test.otpauth rename to account-1.1.otpauth diff --git a/account-1.2.otpauth b/account-1.2.otpauth new file mode 100644 index 0000000..710425f Binary files /dev/null and b/account-1.2.otpauth differ diff --git a/demo.otpauthdb b/backup-1.0.otpauthdb similarity index 100% rename from demo.otpauthdb rename to backup-1.0.otpauthdb diff --git a/backup-1.1.otpauthdb b/backup-1.1.otpauthdb new file mode 100644 index 0000000..236ce69 Binary files /dev/null and b/backup-1.1.otpauthdb differ diff --git a/decrypt_otpauth.py b/decrypt_otpauth.py index 3a920c3..afa4d4e 100644 --- a/decrypt_otpauth.py +++ b/decrypt_otpauth.py @@ -13,6 +13,8 @@ from bpylist.archive_types import uid from Crypto.Cipher import AES +from rncryptor import RNCryptor +from rncryptor import bord class Type(Enum): Unknown = 0 @@ -126,6 +128,13 @@ archiver.update_class_map({'NSMutableString': MutableString}) archiver.update_class_map({'ACOTPFolder': OTPFolder}) archiver.update_class_map({'ACOTPAccount': OTPAccount}) +class RawRNCryptor(RNCryptor): + + def post_decrypt_data(self, data): + """Remove useless symbols which + appear over padding for AES (PKCS#7).""" + data = data[:-bord(data[-1])] + return data class DangerousUnarchive(archiver.Unarchive): @@ -188,7 +197,18 @@ def decrypt_account(encrypted_otpauth_account): # Decode wrapping archive archive = archiver.Unarchive(data).top_object() - # Get IV and key for actual archive + if archive['Version'] == 1.1: + account = decrypt_account_11(archive, password) + elif archive['Version'] == 1.2: + account = decrypt_account_12(archive, password) + else: + print('Encountered unknow file version', archive['Version']) + return + + render_qr_to_terminal(account.otp_uri(), account.type, account.issuer, account.label) + +def decrypt_account_11(archive, password): + # 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() @@ -201,9 +221,17 @@ def decrypt_account(encrypted_otpauth_account): 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) + return OTPAccount.from_dict(archive) +def decrypt_account_12(archive, password): + # Decrypt using RNCryptor + data = data = RawRNCryptor().decrypt(archive['Data'], password) + + # Decode archive + archive = DangerousUnarchive(data).top_object() + + # Construct OTPAccount object from returned dictionary + return OTPAccount.from_dict(archive) @cli.command() @click.option('--encrypted-otpauth-backup', @@ -225,7 +253,20 @@ def decrypt_backup(encrypted_otpauth_backup): # Decode wrapping archive archive = archiver.Unarchive(data).top_object() - # Get IV and key for actual archive + if archive['Version'] == 1.0: + accounts = decrypt_backup_10(archive, password) + elif archive['Version'] == 1.1: + accounts = decrypt_backup_11(archive, password) + else: + print('Encountered unknow file version', archive['Version']) + return + + for account in accounts: + render_qr_to_terminal(account.otp_uri(), account.type, account.issuer, account.label) + input("Press Enter to continue...") + +def decrypt_backup_10(archive, password): + # Get IV and key for actual archive iv = hashlib.sha1(archive['IV'].encode('utf-8')).digest()[:16] salt = archive['Salt'] key = hashlib.sha256((salt + '-' + password).encode('utf-8')).digest() @@ -237,11 +278,16 @@ def decrypt_backup(encrypted_otpauth_backup): # Decode actual archive archive = DangerousUnarchive(data).top_object() - 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...") + return [account for folder in archive['Folders'] for account in folder.accounts] +def decrypt_backup_11(archive, password): + # Decrypt using RNCryptor + data = data = RawRNCryptor().decrypt(archive['WrappedData'], password) + + # Decode archive + archive = DangerousUnarchive(data).top_object() + + return [account for folder in archive['Folders'] for account in folder.accounts] if __name__ == '__main__': cli()