emailrustcryptographypublic-key-encryptionsmime

How to compose a S/MIME encrypted message in Rust?


With so many crates available, I doubt the one for composing an encrypted S/MIME message would be missing. I'm aware of pgp which should handle PGP/MIME. I'm also aware of lettre_email emailmessage and mailparse mail-core which could be used to compose a MIME e-mail message...

If there isn't one, I'm asking if someone already does this so I can copy cat and perhaps publish. Or else I'll be battling with it myself and will appreciate a good head start.

The goal is to encrypt messages at rest while stored on a mail server Samotop. Knowing the recipient's public key, I should be able to wrap-encrypt any incoming message for that recipient so that only the user owning the key will be able to decrypt the message. It may well be that S/MIME is not the right fit for this but I'd fancy to make this usable with existing e-mail clients with S/MIME support.

To start off, I suppose there will be a symmetric key that encrypts the message and this key will be encrypted using asymmetric key for the recipient (potentially for multiple recipients) and included in the payload. Here is a sketch of my ideas.

Random symmetric key is made:

    let mut key = [0u8; 32];
    SystemRandom::new().fill(&mut key).unwrap();

Content gets encrypted:

    // Sealing key used to encrypt data
    let mut sealing_key = SealingKey::new(
        UnboundKey::new(&CHACHA20_POLY1305, key.as_ref()).unwrap(),
        Nonces::new(5),
    );

    // Encrypt data into in_out variable
    sealing_key
        .seal_in_place_append_tag(Aad::empty(), &mut encrypted)
        .unwrap();

Symmetric key gets asymmetrically encrypted for the recipient:

    let enc_key = pub_key.encrypt(&mut rng, PaddingScheme::new_pkcs1v15_encrypt(), &key[..]).expect("failed to encrypt");
    assert_ne!(&key[..], &enc_key[..]);

Now comes the time to compose the encrypted MIME part... ideas? crates? reference implementations? rfc8551


Solution

  • I figured this much. If I have to depend on openssl, I might just as well depend on it externally, running it as a child process. This will enable async io streaming as a bonus which doesn't seem to be supported by rust bindings (yet?).

    fn main() -> Result<(), Box<dyn std::error::Error>> {
        async_std::task::block_on(main_fut())
    }
    
    async fn main_fut() -> Result<(), Box<dyn std::error::Error>> {
        let mut sign = async_process::Command::new("openssl")
            .arg("smime")
            .arg("-stream")
            .arg("-in")
            .arg("test/msg")
            .arg("-sign")
            .arg("-inkey")
            .arg("test/my.key")
            .arg("-signer")
            .arg("test/my.crt")
            .kill_on_drop(true)
            .reap_on_drop(true)
            .stdout(async_process::Stdio::piped())
            .spawn()?;
    
        let mut encrypt = async_process::Command::new("openssl")
            .arg("smime")
            .arg("-stream")
            .arg("-out")
            .arg("test/enc")
            .arg("-encrypt")
            .arg("test/her.crt")
            .kill_on_drop(true)
            .reap_on_drop(true)
            .stdin(async_process::Stdio::piped())
            .spawn()?;
    
        let pipe = async_std::io::copy(
            sign.stdout.as_mut().expect("sign output"),
            encrypt.stdin.as_mut().expect("encrypt input"),
        );
    
        pipe.await?;
    
        println!("sign: {:?}", sign.status().await);
        println!("encrypt: {:?}", encrypt.status().await);
    
        Ok(())
    }