marius-wieschollek/passwords-client

decrypt keychain

Closed this issue · 6 comments

I've got struggle to decrypt the keychain using python pysodium.

This data are from a nextcloud dev instance - so leaking everything here is totally okey.
The cse_password SuperSicher12345 is used.

First, opening the session works as expected and I've got

cse = {
        "keys": {
            "CSEv1r1": "7758874be1c5dcc121c7991084f5ae006e3bcce83bcc3c639aab31e93657ab5fb9312070a76b32a5aebc10efc9bbbbf3fd395ab5cecaa8d7f82838f87dc2723eef512567271d6dd7a5fe1571ad33843f286dedee91fc9087dced3cd7bb9729491c959746a559d83a1d487d8e2a4cf5c5abe7b50d6cb594993a8b1408e2c0fe04212c797198834bc94a4e5760900c87612d6906022f1f35c520cdf456739ae84b2ccbfa26ea25117e3cbf6954940808f39a47975aa9f76373f6942fa8cb2dd4c3b0eb0b6b25d5101c231310d889e0f8ba125ef91ee84a0f560b15e7f303"
        },
        "success": True
    }

But decrypting of the keychain failed https://git.mdns.eu/nextcloud/passwords-client/-/blob/master/src/Encryption/Keychain/CSEv1Keychain.js#L85-92

import pysodium
import binascii
out = {
    "changed": False,
    "failed": False,
    "password": [
        {
            "client": "Passwords Session 26.01.23 18:56 - m@2a02:3032:208:5591:3ede:cf65:4c9:f45f",
            "created": 1674759588,
            "cseKey": "b15ac86a-5c0a-4ece-9c0a-03904d9ccc0a",
            "cseType": "CSEv1r1",
            "customFields": "9de8f88b7f89757d68de395d488eb2330262b610cd04b996bef566898e873e62e5018589d5c3968e97c7",
            "editable": True,
            "edited": 1674759588,
            "favorite": False,
            "folder": "00000000-0000-0000-0000-000000000000",
            "hash": "77b85c9b988d2162ad36fb8620d0f684acbc8344",
            "hidden": False,
            "id": "6b1b8572-1daa-45b3-8df7-acad8af313c9",
            "label": "3d325bc563054f80d188486d38f55261e10a47542212c9b9edf33a6bc647d4e332fd31e6fa07b03c77313419a859da9e735a3775c011a6",
            "notes": "",
            "password": "364847848e926377f924d8ccbbb720d3510b40c3b54d38a354b80a6f2633218c52035dcb15e28b3f32510c7091920118925ae7",
            "revision": "08e51b8a-b1d7-4495-a2b2-3749d4636376",
            "share": None,
            "shared": False,
            "sseType": "none",
            "status": 2,
            "statusCode": "BREACHED",
            "trashed": False,
            "updated": 1674759588,
            "url": "",
            "username": ""
        }
    ]
}

cse = {
    "keys": {
        "CSEv1r1": "7758874be1c5dcc121c7991084f5ae006e3bcce83bcc3c639aab31e93657ab5fb9312070a76b32a5aebc10efc9bbbbf3fd395ab5cecaa8d7f82838f87dc2723eef512567271d6dd7a5fe1571ad33843f286dedee91fc9087dced3cd7bb9729491c959746a559d83a1d487d8e2a4cf5c5abe7b50d6cb594993a8b1408e2c0fe04212c797198834bc94a4e5760900c87612d6906022f1f35c520cdf456739ae84b2ccbfa26ea25117e3cbf6954940808f39a47975aa9f76373f6942fa8cb2dd4c3b0eb0b6b25d5101c231310d889e0f8ba125ef91ee84a0f560b15e7f303"
    },
    "success": True
}


vals = binascii.unhexlify(cse['keys']['CSEv1r1'])
salt = vals[0:pysodium.crypto_box_NONCEBYTES]
key = vals[pysodium.crypto_box_NONCEBYTES:-1]

passwordHash = pysodium.crypto_pwhash(
    pysodium.crypto_box_SEEDBYTES,
    'SuperSicher12345',
    salt,
    pysodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
    pysodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
    pysodium.crypto_pwhash_ALG_ARGON2ID13
)

results in an error about the salt

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/m/.local/lib/python3.10/site-packages/pysodium/__init__.py", line 62, in wrapper
    return func(*args, **kwargs)
  File "/home/m/.local/lib/python3.10/site-packages/pysodium/__init__.py", line 92, in wrapper
    return func(*largs, **kwargs)
  File "/home/m/.local/lib/python3.10/site-packages/pysodium/__init__.py", line 1048, in crypto_pwhash
    if len(salt) != crypto_pwhash_SALTBYTES: raise ValueError("invalid salt")
ValueError: invalid salt
>>> salt
b'wX\x87K\xe1\xc5\xdc\xc1!\xc7\x99\x10\x84\xf5\xae\x00n;\xcc\xe8;\xcc<c'
>>> salt.hex()
'7758874be1c5dcc121c7991084f5ae006e3bcce83bcc3c63'
>>> len(salt)
24

Do you have any idea what's going wrong here?

Ok, I've found that it must use crypto_pwhash_SALTBYTES and not crypto_box_NONCEBYTES. That brings me one step further.

I still need some assistent if possible

import pysodium
import binascii
out = {
    "changed": False,
    "failed": False,
    "password": [
        {
            "client": "Passwords Session 26.01.23 18:56 - m@2a02:3032:208:5591:3ede:cf65:4c9:f45f",
            "created": 1674759588,
            "cseKey": "b15ac86a-5c0a-4ece-9c0a-03904d9ccc0a",
            "cseType": "CSEv1r1",
            "customFields": "9de8f88b7f89757d68de395d488eb2330262b610cd04b996bef566898e873e62e5018589d5c3968e97c7",
            "editable": True,
            "edited": 1674759588,
            "favorite": False,
            "folder": "00000000-0000-0000-0000-000000000000",
            "hash": "77b85c9b988d2162ad36fb8620d0f684acbc8344",
            "hidden": False,
            "id": "6b1b8572-1daa-45b3-8df7-acad8af313c9",
            "label": "3d325bc563054f80d188486d38f55261e10a47542212c9b9edf33a6bc647d4e332fd31e6fa07b03c77313419a859da9e735a3775c011a6",
            "notes": "",
            "password": "364847848e926377f924d8ccbbb720d3510b40c3b54d38a354b80a6f2633218c52035dcb15e28b3f32510c7091920118925ae7",
            "revision": "08e51b8a-b1d7-4495-a2b2-3749d4636376",
            "share": None,
            "shared": False,
            "sseType": "none",
            "status": 2,
            "statusCode": "BREACHED",
            "trashed": False,
            "updated": 1674759588,
            "url": "",
            "username": ""
        }
    ]
}

cse = {
    "keys": {
        "CSEv1r1": "7758874be1c5dcc121c7991084f5ae006e3bcce83bcc3c639aab31e93657ab5fb9312070a76b32a5aebc10efc9bbbbf3fd395ab5cecaa8d7f82838f87dc2723eef512567271d6dd7a5fe1571ad33843f286dedee91fc9087dced3cd7bb9729491c959746a559d83a1d487d8e2a4cf5c5abe7b50d6cb594993a8b1408e2c0fe04212c797198834bc94a4e5760900c87612d6906022f1f35c520cdf456739ae84b2ccbfa26ea25117e3cbf6954940808f39a47975aa9f76373f6942fa8cb2dd4c3b0eb0b6b25d5101c231310d889e0f8ba125ef91ee84a0f560b15e7f303"
    },
    "success": True
}

password='SuperSicher12345'
vals = binascii.unhexlify(cse['keys']['CSEv1r1'])
salt = vals[0:pysodium.crypto_pwhash_SALTBYTES]
text = vals[pysodium.crypto_pwhash_SALTBYTES:-1]

key = pysodium.crypto_pwhash(
    pysodium.crypto_box_SEEDBYTES,
    password,
    salt,
    pysodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
    pysodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
    pysodium.crypto_pwhash_ALG_ARGON2ID13
)

len(key) == pysodium.crypto_secretbox_KEYBYTES

nonce = text[0:pysodium.crypto_secretbox_NONCEBYTES]

len(nonce) == pysodium.crypto_secretbox_NONCEBYTES

ciphertext = text[pysodium.crypto_secretbox_NONCEBYTES:-1]

pysodium.crypto_secretbox_open(ciphertext, nonce, key)

the last step https://git.mdns.eu/nextcloud/passwords-client/-/blob/master/src/Encryption/Keychain/CSEv1Keychain.js#L161
failed. And I've no idea why.


>>> pysodium.crypto_secretbox_open(ciphertext, nonce, key)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/m/.local/lib/python3.10/site-packages/pysodium/__init__.py", line 683, in crypto_secretbox_open
    __check(sodium.crypto_secretbox_open(msg, padded, ctypes.c_ulonglong(len(padded)), nonce, k))
  File "/home/m/.local/lib/python3.10/site-packages/pysodium/__init__.py", line 274, in __check
    raise ValueError
ValueError

I noticed also that the let expectedLength = sodium.crypto_secretbox_NONCEBYTES + sodium.crypto_secretbox_MACBYTES;

results in 40 in python

>>> pysodium.crypto_secretbox_NONCEBYTES + pysodium.crypto_secretbox_MACBYTES
40

that's much shorter as the encrypted/test

text = vals[pysodium.crypto_pwhash_SALTBYTES:-1]
len(text)
204

the exptectedLength is the minimum length. It should always be greater than that

Ha, found it.

The text and ciphertext are both too short. The text is missing the last character and the ciphertext is missing the last two characters.

You need to use text = vals[pysodium.crypto_pwhash_SALTBYTES:] and ciphertext = text[pysodium.crypto_secretbox_NONCEBYTES:]

So no -1, that seems to cut of the last character

vals = binascii.unhexlify(cse['keys']['CSEv1r1'])
salt = vals[0:pysodium.crypto_pwhash_SALTBYTES]
text = vals[pysodium.crypto_pwhash_SALTBYTES:]


key = pysodium.crypto_pwhash(
    pysodium.crypto_box_SEEDBYTES,
    password,
    salt,
    pysodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
    pysodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
    pysodium.crypto_pwhash_ALG_ARGON2ID13
)

len(key) == pysodium.crypto_secretbox_KEYBYTES

nonce = text[0:pysodium.crypto_secretbox_NONCEBYTES]

len(nonce) == pysodium.crypto_secretbox_NONCEBYTES

ciphertext = text[pysodium.crypto_secretbox_NONCEBYTES:]


result = pysodium.crypto_secretbox_open(ciphertext, nonce, key)

print(result)

Thanks, now everything works as expected!