encrypted-json-kv 0.2.1

Easily store encrypted JSON blobs
Documentation
/********************************************************************************
 *   Encrypted KV store for json blobs based on sled                            *
 *   Copyright (C) 2020 Famedly GmbH                                            *
 *                                                                              *
 *   This program is free software: you can redistribute it and/or modify       *
 *   it under the terms of the GNU Affero General Public License as             *
 *   published by the Free Software Foundation, either version 3 of the         *
 *   License, or (at your option) any later version.                            *
 *                                                                              *
 *   This program is distributed in the hope that it will be useful,            *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of             *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the               *
 *   GNU Affero General Public License for more details.                        *
 *                                                                              *
 *   You should have received a copy of the GNU Affero General Public License   *
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.     *
 ********************************************************************************/
use serde::{
    de::{self, Deserialize, Deserializer, Visitor},
    ser::{Serialize, Serializer},
};
use serde_json::Value;

use sodiumoxide::crypto::{pwhash, secretbox};

use thiserror::Error;

use std::fmt;

/// Uses the key derivation function from sodiumoxide to derive a key from a potentially weak
/// passphrase and a salt
pub(crate) fn derive_key(passphrase: &[u8], salt: pwhash::Salt) -> secretbox::Key {
    let mut key = secretbox::Key([0; secretbox::KEYBYTES]);
    {
        let secretbox::Key(ref mut key_bytes) = key;
        pwhash::derive_key(
            key_bytes,
            passphrase,
            &salt,
            pwhash::OPSLIMIT_INTERACTIVE,
            pwhash::MEMLIMIT_INTERACTIVE,
        )
        .unwrap();
    }
    key
}

#[derive(Debug, Clone)]
/// EncryptionWrapper around serde_json::Value, using sodiumoxide's secretbox. Serializes into a
/// string using base64 encoding.
pub struct EncryptedValue {
    nonce: secretbox::Nonce,
    ciphertext: Vec<u8>,
}

impl Serialize for EncryptedValue {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&format!(
            "{}_{}",
            base64::encode(self.nonce.as_ref()),
            base64::encode(&self.ciphertext)
        ))
    }
}
struct EncryptedValueVisitor;

impl<'de> Visitor<'de> for EncryptedValueVisitor {
    type Value = EncryptedValue;

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        formatter.write_str("a string with two base64 encoded values separated with an underscore")
    }

    fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
    where
        E: de::Error,
    {
        let parts: Vec<&str> = s.split("_").collect();
        if parts.len() != 2 {
            return Err(E::custom(format!(
                "expected two base64 encoded values separated with an underscore, got {}",
                parts.len()
            )));
        }
        let (nonce, ciphertext) = match (base64::decode(parts[0]), base64::decode(parts[1])) {
            (Ok(nonce), Ok(ciphertext)) => {
                let nonce = secretbox::Nonce::from_slice(&nonce)
                    .ok_or_else(|| E::custom(format!("nonce part was not 24 bits long")))?;
                (nonce, ciphertext)
            }
            _ => Err(E::custom(format!("couldn't decode one of the bas64 parts")))?,
        };
        Ok(EncryptedValue { nonce, ciphertext })
    }
}

impl<'de> Deserialize<'de> for EncryptedValue {
    fn deserialize<D>(deserializer: D) -> Result<EncryptedValue, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_str(EncryptedValueVisitor)
    }
}

impl EncryptedValue {
    /// Serialize the passed value into a Vec<u8>, encrypt it and then wrap the ciphertext and
    /// nonce into a new instance of Self
    pub fn encrypt(value: &Value, key: &secretbox::Key) -> Self {
        // unwrapping the json encoding here is pretty safe, considering we're trying to turn
        // serde_json::Value into a json String. We could return a result here, but it feels
        // unnecessary
        let plaintext = serde_json::to_vec(value).unwrap();
        let nonce = secretbox::gen_nonce();
        Self {
            nonce,
            ciphertext: secretbox::seal(&plaintext, &nonce, key),
        }
    }

    /// Decrypt the ciphertext with libsodium's secretbox and then deserialize the plaintext.
    pub fn decrypt(&self, key: &secretbox::Key) -> Result<Value, EncryptionError> {
        let plaintext = secretbox::open(&self.ciphertext, &self.nonce, key)
            .map_err(|_| EncryptionError::WrongKey)?;
        Ok(serde_json::from_slice(&plaintext)?)
    }
}

#[derive(Error, Debug)]
pub enum EncryptionError {
    #[error("can't decrypt the ciphertext with the given key")]
    WrongKey,
    #[error("couldn't deserialize the value")]
    Serde(#[from] serde_json::Error),
}