mirror of
https://github.com/nikdoof/decrypt-otpauth-files.git
synced 2025-12-13 08:22:16 +00:00
Initial commit
This commit is contained in:
101
.gitignore
vendored
Normal file
101
.gitignore
vendored
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# dotenv
|
||||||
|
.env
|
||||||
|
|
||||||
|
# virtualenv
|
||||||
|
.venv
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Roland Moers
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
20
Pipfile
Normal file
20
Pipfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[[source]]
|
||||||
|
|
||||||
|
url = "https://pypi.python.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
|
||||||
|
bpylist = "==0.1.4"
|
||||||
|
click = "==6.7"
|
||||||
|
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
|
||||||
|
python_version = "3.6"
|
||||||
47
Pipfile.lock
generated
Normal file
47
Pipfile.lock
generated
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "f412a27aca9ac43baaa70aad1e56e911c64dc2c33b5fac3f039380103c38d197"
|
||||||
|
},
|
||||||
|
"host-environment-markers": {
|
||||||
|
"implementation_name": "cpython",
|
||||||
|
"implementation_version": "3.6.4",
|
||||||
|
"os_name": "posix",
|
||||||
|
"platform_machine": "x86_64",
|
||||||
|
"platform_python_implementation": "CPython",
|
||||||
|
"platform_release": "17.3.0",
|
||||||
|
"platform_system": "Darwin",
|
||||||
|
"platform_version": "Darwin Kernel Version 17.3.0: Thu Nov 9 18:09:22 PST 2017; root:xnu-4570.31.3~1/RELEASE_X86_64",
|
||||||
|
"python_full_version": "3.6.4",
|
||||||
|
"python_version": "3.6",
|
||||||
|
"sys_platform": "darwin"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.6"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.python.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"bpylist": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:ac065c9f7b804a201999ebbc31b02df18e0fc29e03bcb1eac3e63696599b88ce"
|
||||||
|
],
|
||||||
|
"version": "==0.1.4"
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
|
||||||
|
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
|
||||||
|
],
|
||||||
|
"version": "==6.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {}
|
||||||
|
}
|
||||||
33
README.md
Normal file
33
README.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# decrypt-otpauth-files
|
||||||
|
|
||||||
|
This tool allows for decrypting the encrypted backups/account files created by [OTP Auth for iOS](http://cooperrs.de/otpauth.html).
|
||||||
|
|
||||||
|
If you find problems with the file format (in particular security related issues), do not hesitate and file an issue.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Requires:
|
||||||
|
|
||||||
|
- [Python 3.6](https://www.python.org/downloads/)
|
||||||
|
- [pipenv](https://docs.pipenv.org)
|
||||||
|
- An encrypted OTP Auth backup/account file
|
||||||
|
|
||||||
|
```
|
||||||
|
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>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
The project contains two OTP Auth exports for demo purposes:
|
||||||
|
|
||||||
|
* `backup.otpauthdb`: A complete OTP Auth backup
|
||||||
|
* `account.otpauth`: One account exported by OTP Auth
|
||||||
|
|
||||||
|
The password for both files is `abc123`.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
Inspired by [ewdurbin](https://github.com/ewdurbin) and his [evacuate_2STP](https://github.com/ewdurbin/evacuate_2stp) repo.
|
||||||
187
decrypt_otpauth.py
Normal file
187
decrypt_otpauth.py
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import click
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from bpylist import archiver
|
||||||
|
from bpylist.archive_types import uid
|
||||||
|
|
||||||
|
class Type(Enum):
|
||||||
|
Unknown = 0
|
||||||
|
HOTP = 1
|
||||||
|
TOTP = 2
|
||||||
|
|
||||||
|
class Algorithm(Enum):
|
||||||
|
Unknown = 0
|
||||||
|
SHA1 = 1 # Used in case of Unknown
|
||||||
|
SHA256 = 2
|
||||||
|
SHA512 = 3
|
||||||
|
MD5 = 4
|
||||||
|
|
||||||
|
class MutableString:
|
||||||
|
|
||||||
|
def decode_archive(archive):
|
||||||
|
return archive.decode('NS.string')
|
||||||
|
|
||||||
|
class MutableData:
|
||||||
|
|
||||||
|
def decode_archive(archive):
|
||||||
|
return bytes(archive.decode('NS.data'))
|
||||||
|
|
||||||
|
class OTPFolder:
|
||||||
|
name = None
|
||||||
|
accounts = None
|
||||||
|
|
||||||
|
def __init__(self, name, accounts):
|
||||||
|
self.name = name
|
||||||
|
self.accounts = accounts
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<OTPFolder: {self.name}, {self.accounts}>'
|
||||||
|
|
||||||
|
def decode_archive(archive):
|
||||||
|
name = archive.decode('name')
|
||||||
|
accounts = archive.decode('accounts')
|
||||||
|
return OTPFolder(name, accounts)
|
||||||
|
|
||||||
|
class OTPAccount:
|
||||||
|
label = None
|
||||||
|
issue = None
|
||||||
|
secret = None
|
||||||
|
type = None
|
||||||
|
algorithm = None
|
||||||
|
digits = None
|
||||||
|
counter = None
|
||||||
|
period = None
|
||||||
|
refDate = None
|
||||||
|
|
||||||
|
def __init__(self, label, issuer, secret, type, algorithm, digits, counter, period, refDate):
|
||||||
|
self.label = label
|
||||||
|
self.issuer = issuer
|
||||||
|
self.secret = secret
|
||||||
|
self.type = type
|
||||||
|
self.algorithm = algorithm
|
||||||
|
self.digits = digits
|
||||||
|
self.counter = counter
|
||||||
|
self.period = period
|
||||||
|
self.refDate = refDate
|
||||||
|
|
||||||
|
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}>'
|
||||||
|
|
||||||
|
def decode_archive(archive):
|
||||||
|
label = archive.decode("label")
|
||||||
|
issuer = archive.decode("issuer")
|
||||||
|
secret = bytes(archive.decode("secret"))
|
||||||
|
type = Type(archive.decode("type"))
|
||||||
|
algorithm = Algorithm(archive.decode("algorithm"))
|
||||||
|
digits = archive.decode("digits")
|
||||||
|
counter = archive.decode("counter")
|
||||||
|
period = archive.decode("period")
|
||||||
|
refDate = archive.decode("refDate")
|
||||||
|
return OTPAccount(label, issuer, secret, type, algorithm, digits, counter, period, refDate)
|
||||||
|
|
||||||
|
archiver.update_class_map({'NSMutableData': MutableData})
|
||||||
|
archiver.update_class_map({'NSMutableString': MutableString})
|
||||||
|
archiver.update_class_map({'ACOTPFolder': OTPFolder})
|
||||||
|
archiver.update_class_map({'ACOTPAccount': OTPAccount})
|
||||||
|
|
||||||
|
class DangerousUnarchive(archiver.Unarchive):
|
||||||
|
|
||||||
|
def decode_object(self, index):
|
||||||
|
if index == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
obj = self.unpacked_uids.get(index)
|
||||||
|
|
||||||
|
if obj is not None:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
raw_obj = self.objects[index]
|
||||||
|
|
||||||
|
# if obj is a (semi-)primitive type (e.g. str)
|
||||||
|
if not isinstance(raw_obj, dict):
|
||||||
|
return raw_obj
|
||||||
|
|
||||||
|
class_uid = raw_obj.get('$class')
|
||||||
|
if not isinstance(class_uid, uid):
|
||||||
|
raise archiver.MissingClassUID(raw_obj)
|
||||||
|
|
||||||
|
klass = self.class_for_uid(class_uid)
|
||||||
|
obj = klass.decode_archive(archiver.ArchivedObject(raw_obj, self))
|
||||||
|
|
||||||
|
self.unpacked_uids[index] = 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
|
||||||
|
# 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 = DangerousUnarchive(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()
|
||||||
|
# print(archive)
|
||||||
|
|
||||||
|
@click.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):
|
||||||
|
# Get password from user
|
||||||
|
password = getpass.getpass(f'Password for export file {encrypted_otpauth_backup.name}: ')
|
||||||
|
|
||||||
|
# Get IV and key for wrapping archive
|
||||||
|
iv = bytes(16)
|
||||||
|
key = hashlib.sha256('Authenticator'.encode('utf-8')).digest()
|
||||||
|
|
||||||
|
# Decrypt wrapping archive
|
||||||
|
data = AES.new(key, AES.MODE_CBC, iv).decrypt(encrypted_otpauth_backup.read())
|
||||||
|
data = data[:-data[-1]]
|
||||||
|
|
||||||
|
# Decode wrapping archive
|
||||||
|
archive = DangerousUnarchive(data).top_object()
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
# Decrypt actual archive
|
||||||
|
data = AES.new(key, AES.MODE_CBC, iv).decrypt(archive['WrappedData'])
|
||||||
|
data = data[:-data[-1]]
|
||||||
|
|
||||||
|
# Decode actual archive
|
||||||
|
archive = DangerousUnarchive(data).top_object()
|
||||||
|
print(archive)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
BIN
demo.otpauthdb
Normal file
BIN
demo.otpauthdb
Normal file
Binary file not shown.
BIN
test.otpauth
Normal file
BIN
test.otpauth
Normal file
Binary file not shown.
Reference in New Issue
Block a user