base: Add a store key struct
parent
2b5ff82414
commit
28cc5acc87
|
@ -27,7 +27,6 @@ docs = ["encryption", "sled_cryptostore", "messages"]
|
||||||
dashmap= "4.0.1"
|
dashmap= "4.0.1"
|
||||||
serde = { version = "1.0.119", features = ["rc"] }
|
serde = { version = "1.0.119", features = ["rc"] }
|
||||||
serde_json = "1.0.61"
|
serde_json = "1.0.61"
|
||||||
zeroize = "1.2.0"
|
|
||||||
tracing = "0.1.22"
|
tracing = "0.1.22"
|
||||||
|
|
||||||
matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" }
|
matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" }
|
||||||
|
@ -36,6 +35,12 @@ matrix-sdk-crypto = { version = "0.2.0", path = "../matrix_sdk_crypto", optional
|
||||||
# Misc dependencies
|
# Misc dependencies
|
||||||
thiserror = "1.0.23"
|
thiserror = "1.0.23"
|
||||||
sled = "0.34.6"
|
sled = "0.34.6"
|
||||||
|
chacha20poly1305 = "0.7.1"
|
||||||
|
getrandom = "0.2.1"
|
||||||
|
zeroize = { version = "1.2.0", features = ["zeroize_derive"] }
|
||||||
|
pbkdf2 = { version = "0.6.0", default-features = false }
|
||||||
|
hmac = "0.10.1"
|
||||||
|
sha2 = "0.9.2"
|
||||||
futures = "0.3.8"
|
futures = "0.3.8"
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
mod store_key;
|
||||||
|
|
||||||
use std::{convert::TryFrom, path::Path, time::SystemTime};
|
use std::{convert::TryFrom, path::Path, time::SystemTime};
|
||||||
|
|
||||||
use futures::stream::{self, Stream};
|
use futures::stream::{self, Stream};
|
||||||
|
|
|
@ -0,0 +1,278 @@
|
||||||
|
// Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
use chacha20poly1305::{
|
||||||
|
aead::{Aead, Error as EncryptionError, NewAead},
|
||||||
|
ChaCha20Poly1305, Key, Nonce, XChaCha20Poly1305, XNonce,
|
||||||
|
};
|
||||||
|
use getrandom::getrandom;
|
||||||
|
use hmac::Hmac;
|
||||||
|
use pbkdf2::pbkdf2;
|
||||||
|
use sha2::Sha256;
|
||||||
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
const VERSION: u8 = 1;
|
||||||
|
const KEY_SIZE: usize = 32;
|
||||||
|
const NONCE_SIZE: usize = 12;
|
||||||
|
const XNONCE_SIZE: usize = 24;
|
||||||
|
const KDF_SALT_SIZE: usize = 32;
|
||||||
|
#[cfg(not(test))]
|
||||||
|
const KDF_ROUNDS: u32 = 200_000;
|
||||||
|
#[cfg(test)]
|
||||||
|
const KDF_ROUNDS: u32 = 1000;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error(transparent)]
|
||||||
|
Serialization(#[from] serde_json::Error),
|
||||||
|
#[error("Error encrypting or decrypting an event {0}")]
|
||||||
|
Encryption(String),
|
||||||
|
#[error("Unknown ciphertext version")]
|
||||||
|
InvalidVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EncryptionError> for Error {
|
||||||
|
fn from(e: EncryptionError) -> Self {
|
||||||
|
Error::Encryption(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct EncryptedEvent {
|
||||||
|
version: u8,
|
||||||
|
ciphertext: Vec<u8>,
|
||||||
|
nonce: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Version specific info for the key derivation method that is used.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum KdfInfo {
|
||||||
|
Pbkdf2ToChaCha20Poly1305 {
|
||||||
|
/// The number of PBKDF rounds that were used when deriving the store key.
|
||||||
|
rounds: u32,
|
||||||
|
/// The salt that was used when the passphrase was expanded into a store key.
|
||||||
|
kdf_salt: Vec<u8>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Version specific info for encryption method that is used to encrypt our
|
||||||
|
/// store key.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum CipherTextInfo {
|
||||||
|
ChaCha20Poly1305 {
|
||||||
|
/// The nonce that was used to encrypt the ciphertext.
|
||||||
|
nonce: Vec<u8>,
|
||||||
|
/// The encrypted store key.
|
||||||
|
ciphertext: Vec<u8>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An encrypted version of our store key, this can be safely stored in a
|
||||||
|
/// database.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct EncryptedStoreKey {
|
||||||
|
/// Info about the key derivation method that was used to expand the
|
||||||
|
/// passphrase into an encryption key.
|
||||||
|
pub kdf_info: KdfInfo,
|
||||||
|
/// The ciphertext with it's accompanying additional data that is needed to
|
||||||
|
/// decrypt the store key.
|
||||||
|
pub ciphertext_info: CipherTextInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A store key that can be used to encrypt entries in the store.
|
||||||
|
#[derive(Debug, Zeroize, PartialEq)]
|
||||||
|
pub struct StoreKey {
|
||||||
|
inner: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StoreKey {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut key = vec![0u8; KEY_SIZE];
|
||||||
|
getrandom(&mut key).expect("Can't generate new store key");
|
||||||
|
|
||||||
|
Self { inner: key }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Vec<u8>> for StoreKey {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||||
|
if value.len() != KEY_SIZE {
|
||||||
|
Err(())
|
||||||
|
} else {
|
||||||
|
Ok(Self { inner: value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StoreKey {
|
||||||
|
/// Generate a new random store key.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_key(passphrase: &str, salt: &[u8], rounds: u32) -> Zeroizing<Vec<u8>> {
|
||||||
|
let mut key = Zeroizing::from(vec![0u8; KEY_SIZE]);
|
||||||
|
pbkdf2::<Hmac<Sha256>>(passphrase.as_bytes(), &salt, rounds, &mut *key);
|
||||||
|
key
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the store key.
|
||||||
|
fn key(&self) -> &Key {
|
||||||
|
Key::from_slice(&self.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypt and export our store key using the given passphrase.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `passphrase` - The passphrase that should be used to encrypt the
|
||||||
|
/// store key.
|
||||||
|
pub fn export(&self, passphrase: &str) -> EncryptedStoreKey {
|
||||||
|
let mut salt = vec![0u8; KDF_SALT_SIZE];
|
||||||
|
getrandom(&mut salt).expect("Can't generate new random store key");
|
||||||
|
|
||||||
|
let key = StoreKey::expand_key(passphrase, &salt, KDF_ROUNDS);
|
||||||
|
let key = Key::from_slice(key.as_ref());
|
||||||
|
let cipher = ChaCha20Poly1305::new(&key);
|
||||||
|
|
||||||
|
let mut nonce = vec![0u8; NONCE_SIZE];
|
||||||
|
getrandom(&mut nonce).expect("Can't generate new random nonce for the store key");
|
||||||
|
|
||||||
|
let ciphertext = cipher
|
||||||
|
.encrypt(Nonce::from_slice(nonce.as_ref()), self.inner.as_slice())
|
||||||
|
.expect("Can't encrypt store key");
|
||||||
|
|
||||||
|
EncryptedStoreKey {
|
||||||
|
kdf_info: KdfInfo::Pbkdf2ToChaCha20Poly1305 {
|
||||||
|
rounds: KDF_ROUNDS,
|
||||||
|
kdf_salt: salt,
|
||||||
|
},
|
||||||
|
ciphertext_info: CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_nonce() -> Vec<u8> {
|
||||||
|
let mut nonce = vec![0u8; XNONCE_SIZE];
|
||||||
|
getrandom(&mut nonce).expect("Can't generate nonce");
|
||||||
|
nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt(&self, event: &impl Serialize) -> Result<EncryptedEvent, Error> {
|
||||||
|
let event = serde_json::to_vec(event)?;
|
||||||
|
|
||||||
|
let nonce = StoreKey::get_nonce();
|
||||||
|
let cipher = XChaCha20Poly1305::new(self.key());
|
||||||
|
let xnonce = XNonce::from_slice(&nonce);
|
||||||
|
|
||||||
|
let ciphertext = cipher.encrypt(xnonce, event.as_ref())?;
|
||||||
|
|
||||||
|
Ok(EncryptedEvent {
|
||||||
|
version: VERSION,
|
||||||
|
ciphertext,
|
||||||
|
nonce,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt<T: for<'b> Deserialize<'b>>(&self, event: EncryptedEvent) -> Result<T, Error> {
|
||||||
|
if event.version != VERSION {
|
||||||
|
return Err(Error::InvalidVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cipher = XChaCha20Poly1305::new(self.key());
|
||||||
|
let nonce = XNonce::from_slice(&event.nonce);
|
||||||
|
let plaintext = cipher.decrypt(nonce, event.ciphertext.as_ref())?;
|
||||||
|
|
||||||
|
Ok(serde_json::from_slice(&plaintext)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore a store key from an encrypted export.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `passphrase` - The passphrase that should be used to encrypt the
|
||||||
|
/// store key.
|
||||||
|
///
|
||||||
|
/// * `encrypted` - The exported and encrypted version of the store key.
|
||||||
|
pub fn import(passphrase: &str, encrypted: EncryptedStoreKey) -> Result<Self, EncryptionError> {
|
||||||
|
let key = match encrypted.kdf_info {
|
||||||
|
KdfInfo::Pbkdf2ToChaCha20Poly1305 { rounds, kdf_salt } => {
|
||||||
|
Self::expand_key(passphrase, &kdf_salt, rounds)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = Key::from_slice(key.as_ref());
|
||||||
|
|
||||||
|
let decrypted = match encrypted.ciphertext_info {
|
||||||
|
CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext } => {
|
||||||
|
let cipher = ChaCha20Poly1305::new(&key);
|
||||||
|
let nonce = Nonce::from_slice(&nonce);
|
||||||
|
cipher.decrypt(nonce, ciphertext.as_ref())?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { inner: decrypted })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::StoreKey;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generating() {
|
||||||
|
StoreKey::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encrypting() {
|
||||||
|
let passphrase = "it's a secret to everybody";
|
||||||
|
let store_key = StoreKey::new();
|
||||||
|
|
||||||
|
let encrypted = store_key.export(passphrase);
|
||||||
|
let decrypted = StoreKey::import(passphrase, encrypted).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(store_key, decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encrypting_events() {
|
||||||
|
let event = json!({
|
||||||
|
"content": {
|
||||||
|
"body": "Bee Gees - Stayin' Alive",
|
||||||
|
"info": {
|
||||||
|
"duration": 2140786,
|
||||||
|
"mimetype": "audio/mpeg",
|
||||||
|
"size": 1563685
|
||||||
|
},
|
||||||
|
"msgtype": "m.audio",
|
||||||
|
"url": "mxc://example.org/ffed755USFFxlgbQYZGtryd"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let store_key = StoreKey::new();
|
||||||
|
|
||||||
|
let encrypted = store_key.encrypt(&event).unwrap();
|
||||||
|
let decrypted: Value = store_key.decrypt(encrypted).unwrap();
|
||||||
|
assert_eq!(event, decrypted);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue