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