Legrandin / pycryptodome

A self-contained cryptographic library for Python
https://www.pycryptodome.org
Other
2.83k stars 500 forks source link

En-/decrypt S/MIME messages #437

Open larsrinn opened 4 years ago

larsrinn commented 4 years ago

Hey there!

My interest in pycryptodome is due to my task to en-/decrypt email messages using RSASSA-OAEP key encryption and SHA512 hashing, which seems to be unsupported in openssl (somehow SHA1 is the hard coded default without a way to change this http://openssl.6102.n7.nabble.com/RSA-OAEP-with-sha256-td16377.html ).

On the way to figure this out, I stumbled upon PKI.js which I used to create test messages by the S/MIME encryption example.

The key and encrypted message are (not yet with SHA256):

Setup

Private key (SHA-1, RSA-PSS):

-----BEGIN PRIVATE KEY-----
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDnQHFwXtZa8ZuB
osutuW6kucmGMFqipPrFI8hRKp2ffILRHNeNMNobZ8pyL+FuyQoL7waa4VCsZKSc
CxgVfq5Ie4koOt2d6xbjfyp/ToHCLQyFvKvUPDjdovQknnl6yyi4O7AQLOsIs54B
i9P6JN05aFVAfvpho8GazEyKTARC8g0l8S88c7Wiij+GSqJGsEpBrFvibGzGlopr
+alUl/zJrTeTo6iGPlVx8HEHkNbel4WmoTk/OfLlV1iyQmoy0IskLcumyc0LThfT
8aEVGMzYlX5esig4GnquuLeEYYUNyJYUep+6gi94IB9iCI7fkCliguAJ0SUmyF4b
FKzgeqT5AgMBAAECgf88S9FIO3IoxFaHtqrk4TS4PrkNBA6d2eaJAIt6nmH687wo
Shzp85LrEmT24QUmncTA19IhEB641IUXKs1czWsj+xIIK1Edm+6b4sxx5UZwGs7p
EAi9jfZF0/dUlP2XxuEXKHj/vraJzkukm5Dp6DLGhS21Y2Zljw0sD5jldmymB3fy
fk/L4Vv1ailIOlQn0uRAY9Zheo+ckqmKiC2nuBFhwEqy6aFqS6hwSET9+UIWb/BX
JQPD9wTnCCksTlHU5sTCfdv6W9w6NmT1zypG20ow75VH89t+YNL15VUh9n5LWMvR
ZvTxQOZNPLEiicyfBU3Rm/Bt3EvojLzAuo1/YEECgYEA+3SsqBygQm/v9s8qqYgf
8Gl5XWpUFrOYd9PNI28KVg/WdAhbC6bTmUJOtJSajzn7YS9WXnSsyutfT4tKsmSy
itlUZULFQQAqPYhXyUp1cHTWx2UVRMf//7k4xdfNgR4caN49yUpNk3zoirss/dx2
TInMHJP3Kgv2p3LqaVx0FtkCgYEA625MKffHWPgGKzJG23f8g74wvDtvz37S1cCo
U1JKo3Z1QkwtdjjeeCwGkisUAHThExn3/pybZ8QUjpkxRiOPts4qcQ3ZEAcGnyVt
QeN7y21tH5x44bn8f2mhVj0u26OU0X5YjD91dHVrYV4QjYYwM8ETvJoYZXWJPprf
Q5tPayECgYEA+l+vNujh7aUVc1PN7+YZn0D2Vjx0I/KJYu7iuGBtE0pLx1c2iICA
+n4abhX1W51pHtiKkBxunNIGIebY5o37dON29CiqzdEDPieO+V+JVgMQhJOyvLzD
uvXLgLsi5Wh88zIupUm1uqBJzzEWWGN26zjdoqr2FMi6vPpgS66B7OkCgYBKgyAr
8DXxnJ4nMcRnVbRf5eP6zzz2CQeli1I6/MKOtcEq+H0y+5C9rAFwZZ3w/wz1RLTO
qrYsw0xWTXng3wRvMRURrvZSMkcQO4I25k6Z9cohxR4lIv4dPUtIxhh8f2tsWnaf
/L2p3DfeGy1V/XBoEOW0PXkXM3n6jH25IgCiwQKBgGcl8z7VxP37eH7LZ5hPPpac
lTILytes2YoZElAANhRwROVAM637e9uZxlFc/F53wk+Skk4sj9q6YILgFSj6Ivb0
QmCqStCmxI5Z7xEsOqQvX36x1FJQm86d8H4rCf/48+zUFpFplHsfZHzD55LHPJyV
oyHd0SssHZQrRcUnwBm1
-----END PRIVATE KEY-----

Message (AES-CBC, 128 content encryption algorithm length, RSA-OAEP with SHA-1)

Content-Type: application/pkcs7-mime; name=smime.p7m;
 smime-type=enveloped-data; charset=binary
Content-Description: Enveloped Data
Content-Disposition: attachment; filename=smime.p7m
Content-Transfer-Encoding: base64
From: sender@example.com
To: recipient@example.com
Subject: Example S/MIME encrypted message
Date: Wed, 29 Jul 2020 15:41:10 +0000
Message-Id: <1596037270614-7adbbb2d-e14a3f11-18bca35a@example.com>
MIME-Version: 1.0

MIIBrQYJKoZIhvcNAQcDoIIBnjCCAZoCAQIxggE/MIIBOwIBADAjMB4xHDAJBgNVBAYTAlJVMA8G
A1UEAx4IAFQAZQBzAHQCAQEwDQYJKoZIhvcNAQEHMAAEggEAPRh/QyvFaqvhNQh9DSJN+aGiYDoL
JuRc2Ye0EgVgDB3OFYOz/KsIT3alzssgzcBcbJHJQzGQPApqcX4aA1mPDWwxw0rD+RRrgy7zydun
OPAMV2yfXZtIQOibbfONsJGD2L9q54XOxrYQI7NJtQ0JBC4CQf3T2vKQJaWhPSU64hrZHT0RcTIc
alFj7O30ODRa5vuy/jIf+08npR638vIK0lVfT8fbPtAXBlQ9kmHO7KyEGbW98z9X8Yl4JUNa6nbs
neg7MSZBbDXBPMrseEacQkVQtR8ibwR+QCiOGD3zrQn3WTtqKM+pRI+vywx/LtAdnDRtaIxmYgS2
YCRCsT+DNTCABgkqhkiG9w0BBwEwHQYJYIZIAWUDBAECBBA1AHWGxXaXBHhMVfb3NjPtoIAEIP/F
pg64qTCmbNcAfivD1rG/vKpI0lPg0VZBo0hYOKuTAAAAAA==

Decrypt using openssl

Using openssl, I can successfully decrypt this (because it's SHA-1)

> openssl cms -decrypt -inkey private.pem -in message.msg

# output:
> Successfully decrypted!

Decrypting using pycryptodome

To not mess with the MIME details for now, I just copied the actual content of the mail to a file called encrypted_b64. Now, I followed the approach from the Encrypt data with RSA section in the documentation (I just adopted it for the fact that my input is in base64).

import base64
from pathlib import Path

from Crypto.Cipher import PKCS1_OAEP, AES
from Crypto.PublicKey import RSA

# Read in private key
private_key = RSA.import_key(Path("private.pem").read_bytes())

# Read in base64 encrypted content
encrypted_b64 = Path("encrypted_b64").read_bytes()
encrypted = base64.b64decode(encrypted_b64)

# Extract encrypted session key, nonce, tag and cipher text from the encrypted content
# This yields equivalent results as reading directly from the file (as performed in the example)

key_size = private_key.size_in_bytes()
enc_session_key = encrypted[:key_size]
nonce = encrypted[(key_size) : (key_size + 16)]
tag = encrypted[(key_size + 16) : (key_size + 32)]
ciphertext = encrypted[(key_size + 32) :]

# Decrypt the session key with the private RSA key
cipher_rsa = PKCS1_OAEP.new(private_key)
session_key = cipher_rsa.decrypt(enc_session_key)  # <- this errors

print(session_key)

# Decrypt the data with the AES session key
cipher_aes = AES.new(session_key, AES.MODE_EAX, nonce)
data = cipher_aes.decrypt_and_verify(ciphertext, tag)
print(data.decode("utf-8"))

Here's all the code and data: decrypt_smime_pycryptodome.zip

Am I doing something wrong or is this an issue in pycryptodome? At least I think the documentation is a bit missleading. I'd be happy to help out, when this is solved.

texadactyl commented 4 years ago

On page https://pycryptodome.readthedocs.io/en/latest/src/future.html note "Add support for CMS/PKCS#7".

@Legrandin This looks like an enhancement request.

@larsrinn There are currently a couple of ways to do what you want within Python 3 that I know of:

1) Use the Python 3 subprocess function to run openssl cms, carefully specifying an output file that your code can subsequently open and read. I use this in a couple of my projects (for other reasons) and found it reliable.

2) Use another s/MIME project that does perform several s/MIME functions: https://m2crypto.readthedocs.io/en/latest/howto.smime.html

texadactyl commented 4 years ago

@larsrinn Here is one starter kit for the subprocess case:

import subprocess
import sys

DIR = the place where the artifacts are stored
KEY_FILE = DIR + 'private.pem'
INPUT_FILE = DIR + 'message.msg'
OUTPUT_FILE = DIR + 'clear_message.msg'
CMD = ['openssl', 'cms', '-decrypt', '-inkey', KEY_FILE, '-in', 
       INPUT_FILE, '-out', OUTPUT_FILE]

result = subprocess.run(CMD, check=False,
                        stdout=subprocess.PIPE,
                        stderr=subprocess.STDOUT)
if result.returncode != 0:
    print("*** Oops, subprocess.run failed, details:\n{}".format(result))
    sys.exit(86)

with open(OUTPUT_FILE, "r") as fd:
    cleartext = fd.readlines()
print("Cleartext message:\n{}".format(cleartext))

Stdout:

Cleartext message:
['Successfully decrypted!']
larsrinn commented 4 years ago

Hi @texadactyl

thanks for your answers. However, as I've mentioned in the issue, I need SHA512 hashing, wich seems to be unsupported by openssl. Currently I'm using an approach similar to the own you're proposing with subprocess.run, but I only need to sign/verify and not encrypt/decrypt the messages.

I didn't see the Add support for CMS/PKCS#7 point on the future plans list. Actually my presumption was the body of the mail is simply the base64 encoded result of the encryption performed in this example: https://pycryptodome.readthedocs.io/en/latest/src/examples.html

I'm happy to help with the implementation of CMS, but I've no idea on how to get started, except for looking at RFC5652. Since my knowledge in cryptography is very limited, I don't know if I'd be a huge help, though.