mirror of
https://github.com/nikdoof/decrypt-otpauth-files.git
synced 2025-12-13 08:22:16 +00:00
restore single account behavior, refactor a bit :)
This commit is contained in:
@@ -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 <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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user