Skip to content

Commit

Permalink
Initial support for decrypting items from a Collection (Organization)
Browse files Browse the repository at this point in the history
New:
Initial support for decrypting items from a Collection (Organization).

Other:
Code refactoring.
  • Loading branch information
GurpreetKang committed Aug 18, 2020
1 parent 17143b0 commit c26de27
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 55 deletions.
201 changes: 147 additions & 54 deletions BitwardenDecrypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,83 @@
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.asymmetric import rsa, padding as asymmetricpadding
from cryptography.hazmat.primitives.serialization import load_der_private_key
except ModuleNotFoundError:
print("This script depends on the 'cryptography' package")
print("pip install cryptography")
exit(1)


def getBitwardenSecrets(email, password, kdfIterations, encKey, encPrivateKey):
BitwardenSecrets = {}
BitwardenSecrets['email'] = email
BitwardenSecrets['kdfIterations'] = kdfIterations
BitwardenSecrets['MasterPassword'] = password
BitwardenSecrets['ProtectedSymmetricKey'] = encKey
BitwardenSecrets['ProtectedRSAPrivateKey'] = encPrivateKey


kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=bytes(BitwardenSecrets['email'], 'utf-8'),
iterations=BitwardenSecrets['kdfIterations'],
backend=default_backend()
)
BitwardenSecrets['MasterKey'] = kdf.derive(BitwardenSecrets['MasterPassword'])
BitwardenSecrets['MasterKey_b64'] = base64.b64encode(BitwardenSecrets['MasterKey']).decode('utf-8')


kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=bytes(BitwardenSecrets['MasterPassword']),
iterations=1,
backend=default_backend()
)
BitwardenSecrets['MasterPasswordHash'] = base64.b64encode(kdf.derive(BitwardenSecrets['MasterKey'])).decode('utf-8')


hkdf = HKDFExpand(
algorithm=hashes.SHA256(),
length=32,
info=b"enc",
backend=default_backend()
)
BitwardenSecrets['StretchedEncryptionKey'] = hkdf.derive(BitwardenSecrets['MasterKey'])
BitwardenSecrets['StretchedEncryptionKey_b64'] = base64.b64encode(BitwardenSecrets['StretchedEncryptionKey']).decode('utf-8')

hkdf = HKDFExpand(
algorithm=hashes.SHA256(),
length=32,
info=b"mac",
backend=default_backend()
)
BitwardenSecrets['StretchedMacKey'] = hkdf.derive(BitwardenSecrets['MasterKey'])
BitwardenSecrets['StretchedMacKey_b64'] = base64.b64encode(BitwardenSecrets['StretchedMacKey']).decode('utf-8')

BitwardenSecrets['StretchedMasterKey'] = BitwardenSecrets['StretchedEncryptionKey'] + BitwardenSecrets['StretchedMacKey']
BitwardenSecrets['StretchedMasterKey_b64'] = base64.b64encode(BitwardenSecrets['StretchedMasterKey']).decode('utf-8')

BitwardenSecrets['GeneratedSymmetricKey'], \
BitwardenSecrets['GeneratedEncryptionKey'], \
BitwardenSecrets['GeneratedMACKey'] = decryptMasterEncryptionKey(BitwardenSecrets['ProtectedSymmetricKey'], BitwardenSecrets['StretchedEncryptionKey'], BitwardenSecrets['StretchedMacKey'])
BitwardenSecrets['GeneratedSymmetricKey_b64'] = base64.b64encode(BitwardenSecrets['GeneratedSymmetricKey']).decode('utf-8')
BitwardenSecrets['GeneratedEncryptionKey_b64'] = base64.b64encode(BitwardenSecrets['GeneratedEncryptionKey']).decode('utf-8')
BitwardenSecrets['GeneratedMACKey_b64'] = base64.b64encode(BitwardenSecrets['GeneratedMACKey']).decode('utf-8')


def decodeMasterEncryptionKey(CipherString, masterkey, mastermac):
encType = int(CipherString.split(".")[0]) # Not Currently Used
BitwardenSecrets['RSAPrivateKey'] = decryptRSAPrivateKey(BitwardenSecrets['ProtectedRSAPrivateKey'], \
BitwardenSecrets['GeneratedEncryptionKey'], \
BitwardenSecrets['GeneratedMACKey'])

return(BitwardenSecrets)



def decryptMasterEncryptionKey(CipherString, masterkey, mastermac):
encType = int(CipherString.split(".")[0]) # Not Currently Used, Assuming EncryptionType: 2
iv = base64.b64decode(CipherString.split(".")[1].split("|")[0])
ciphertext = base64.b64decode(CipherString.split(".")[1].split("|")[1])
mac = base64.b64decode(CipherString.split(".")[1].split("|")[2])
Expand All @@ -66,11 +134,12 @@ def decodeMasterEncryptionKey(CipherString, masterkey, mastermac):
h.update(iv)
h.update(ciphertext)
calculatedMAC = h.finalize()

if mac != calculatedMAC:
print("ERROR: MAC did not match. Master Encryption Key was not decrypted.")
exit(1)


unpadder = padding.PKCS7(128).unpadder()
cipher = Cipher(algorithms.AES(masterkey), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
Expand All @@ -90,12 +159,43 @@ def decodeMasterEncryptionKey(CipherString, masterkey, mastermac):
return([stretchedmasterkey,enc,mac])


def decodeCipherString(CipherString, key, mackey):
def decryptRSAPrivateKey(CipherString, key, mackey):
if not CipherString:
return(None)


encType = int(CipherString.split(".")[0]) # Not Currently Used
encType = int(CipherString.split(".")[0]) # Not Currently Used, Assuming EncryptionType: 2
iv = base64.b64decode(CipherString.split(".")[1].split("|")[0])
ciphertext = base64.b64decode(CipherString.split(".")[1].split("|")[1])
mac = base64.b64decode(CipherString.split(".")[1].split("|")[2])


# Calculate CipherString MAC
h = hmac.HMAC(mackey, hashes.SHA256(), backend=default_backend())
h.update(iv)
h.update(ciphertext)
calculatedMAC = h.finalize()

if mac == calculatedMAC:
unpadder = padding.PKCS7(128).unpadder()
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
decrypted = decryptor.update(ciphertext) + decryptor.finalize()
cleartext = unpadder.update(decrypted) + unpadder.finalize()

return(cleartext)

else:
return("ERROR: MAC did not match. RSA Private Key not decrypted.")



def decryptCipherString(CipherString, key, mackey):
if not CipherString:
return(None)


encType = int(CipherString.split(".")[0]) # Not Currently Used, Assuming EncryptionType: 2
iv = base64.b64decode(CipherString.split(".")[1].split("|")[0])
ciphertext = base64.b64decode(CipherString.split(".")[1].split("|")[1])
mac = base64.b64decode(CipherString.split(".")[1].split("|")[2])
Expand All @@ -120,10 +220,21 @@ def decodeCipherString(CipherString, key, mackey):
return("ERROR: MAC did not match. CipherString not decrypted.")


def decryptRSA(CipherString, key):
encType = int(CipherString.split(".")[0]) # Not Currently Used, Assuming EncryptionType: 4
ciphertext = base64.b64decode(CipherString.split(".")[1].split("|")[0])
private_key = load_der_private_key(key, password=None, backend=default_backend())

plaintext = private_key.decrypt(ciphertext, asymmetricpadding.OAEP(mgf=asymmetricpadding.MGF1(algorithm=hashes.SHA1()), \
algorithm=hashes.SHA1(), \
label=None))

return(plaintext)


def decryptBitwardenJSON(inputfile):
BitwardenSecrets = {}
decodedEntries = {}
decryptedEntries = {}

try:
with open(inputfile) as f:
Expand All @@ -132,62 +243,34 @@ def decryptBitwardenJSON(inputfile):
print("ERROR: " + inputfile + " not found.")
exit(1)

BitwardenSecrets = getBitwardenSecrets(datafile["userEmail"], \
getpass.getpass().encode("utf-8"), \
datafile["kdfIterations"], \
datafile["encKey"], \
datafile["encPrivateKey"] )

BitwardenSecrets['email'] = datafile["userEmail"]
BitwardenSecrets['kdfIterations'] = datafile["kdfIterations"]
BitwardenSecrets['MasterPassword'] = getpass.getpass().encode("utf-8")
BitwardenSecrets['ProtectedSymmetricKey'] = datafile["encKey"]


kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=bytes(BitwardenSecrets['email'], 'utf-8'),
iterations=BitwardenSecrets['kdfIterations'],
backend=default_backend()
)
BitwardenSecrets['MasterKey'] = kdf.derive(BitwardenSecrets['MasterPassword'])
BitwardenSecrets['MasterKey_b64'] = base64.b64encode(BitwardenSecrets['MasterKey']).decode('utf-8')


hkdf = HKDFExpand(
algorithm=hashes.SHA256(),
length=32,
info=b"enc",
backend=default_backend()
)
BitwardenSecrets['StretchedEncryptionKey'] = hkdf.derive(BitwardenSecrets['MasterKey'])
BitwardenSecrets['StretchedEncryptionKey_b64'] = base64.b64encode(BitwardenSecrets['StretchedEncryptionKey']).decode('utf-8')

hkdf = HKDFExpand(
algorithm=hashes.SHA256(),
length=32,
info=b"mac",
backend=default_backend()
)
BitwardenSecrets['StretchedMacKey'] = hkdf.derive(BitwardenSecrets['MasterKey'])
BitwardenSecrets['StretchedMacKey_b64'] = base64.b64encode(BitwardenSecrets['StretchedMacKey']).decode('utf-8')


BitwardenSecrets['StretchedMasterKey'] = BitwardenSecrets['StretchedEncryptionKey'] + BitwardenSecrets['StretchedMacKey']
BitwardenSecrets['StretchedMasterKey_b64'] = base64.b64encode(BitwardenSecrets['StretchedMasterKey']).decode('utf-8')


BitwardenSecrets['GeneratedSymmetricKey'], \
BitwardenSecrets['GeneratedEncryptionKey'], \
BitwardenSecrets['GeneratedMACKey'] = decodeMasterEncryptionKey(datafile["encKey"], BitwardenSecrets['StretchedEncryptionKey'], BitwardenSecrets['StretchedMacKey'])
BitwardenSecrets['GeneratedSymmetricKey_b64'] = base64.b64encode(BitwardenSecrets['GeneratedSymmetricKey']).decode('utf-8')
BitwardenSecrets['GeneratedEncryptionKey_b64'] = base64.b64encode(BitwardenSecrets['GeneratedEncryptionKey']).decode('utf-8')
BitwardenSecrets['GeneratedMACKey_b64'] = base64.b64encode(BitwardenSecrets['GeneratedMACKey']).decode('utf-8')
BitwardenSecrets['OrgSecrets'] = {}
encOrgKeys = list(datafile["encOrgKeys"])

for i in encOrgKeys:
BitwardenSecrets['OrgSecrets'][i] = decryptRSA(datafile["encOrgKeys"][i], BitwardenSecrets['RSAPrivateKey'])


regexPattern = re.compile(r"\d\.[^,]+\|[^,]+=+")

for a in datafile:

if a.startswith('folders_'):
group = "folders"
elif a.startswith('ciphers_'):
group = "items"
elif a.startswith('organizations_'):
group = "organizations"
elif a.startswith('collections_'):
group = "collections"
else:
group = None

Expand All @@ -205,8 +288,16 @@ def decryptBitwardenJSON(inputfile):
if type(groupItem) is dict:
tempString = json.dumps(c)

try:
if (len(groupItem['organizationId'])) > 0:
encKey = BitwardenSecrets['OrgSecrets'][groupItem['organizationId']][0:32]
macKey = BitwardenSecrets['OrgSecrets'][groupItem['organizationId']][32:64]
except Exception:
encKey = BitwardenSecrets['GeneratedEncryptionKey']
macKey = BitwardenSecrets['GeneratedMACKey']

for match in regexPattern.findall(tempString):
jsonEscapedString = json.JSONEncoder().encode(decodeCipherString(match, BitwardenSecrets['GeneratedEncryptionKey'], BitwardenSecrets['GeneratedMACKey']))
jsonEscapedString = json.JSONEncoder().encode(decryptCipherString(match, encKey, macKey))
jsonEscapedString = jsonEscapedString[1:(len(jsonEscapedString)-1)]
tempString = tempString.replace(match, jsonEscapedString)

Expand All @@ -216,9 +307,9 @@ def decryptBitwardenJSON(inputfile):

groupItemsList.append(json.loads(tempString))

decodedEntries[group] = groupItemsList
decryptedEntries[group] = groupItemsList

return(decodedEntries)
return(json.dumps(decryptedEntries, indent=2))


def main():
Expand All @@ -227,7 +318,9 @@ def main():
else:
inputfile = "data.json"

print(json.dumps(decryptBitwardenJSON(inputfile), indent=2))
decryptedJSON = decryptBitwardenJSON(inputfile)
print(decryptedJSON)


if __name__ == "__main__":
main()
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Outputs JSON containing:
- Secure Notes
- Identities
- Folders
- Organizations
- Collections

*Note: Outputs (almost) all key/value pairs, including ones you probably don't care about.*

Expand Down Expand Up @@ -44,7 +46,8 @@ https://paypal.me/GurpreetKang
- ~~No validation of the CipherString.
I.e. No verification of the MAC before decrypting.~~ Now verifies the MAC.
- Can only decrypt EncryptionType: 2 (AesCbc256_HmacSha256_B64). At the time of writing this is the default used for all entries in the personal vault.
- Does not decrypt anything from a Collection (Organization).
- ~~Does not decrypt anything from a Collection (Organization).~~<br>Initial support for decrypting items from a Collection (Organization). This adds support for decrypting EncryptionType: 4 (Rsa2048_OaepSha1_B64)
<br> *Note: Only tested with Personal/Free account (1 Organizaion).*


## To Do
Expand Down

0 comments on commit c26de27

Please sign in to comment.