mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-05 00:37:04 +02:00
[WIP] Encryption for secure values (#183)
This commit is contained in:
31
src-tauri/yaak-crypto/src/commands.rs
Normal file
31
src-tauri/yaak-crypto/src/commands.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use crate::error::Result;
|
||||
use crate::manager::EncryptionManagerExt;
|
||||
use tauri::{command, Runtime, WebviewWindow};
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn enable_encryption<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
workspace_id: &str,
|
||||
) -> Result<()> {
|
||||
window.crypto().ensure_workspace_key(workspace_id)?;
|
||||
window.crypto().reveal_workspace_key(workspace_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn reveal_workspace_key<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
workspace_id: &str,
|
||||
) -> Result<String> {
|
||||
Ok(window.crypto().reveal_workspace_key(workspace_id)?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn set_workspace_key<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
workspace_id: &str,
|
||||
key: &str,
|
||||
) -> Result<()> {
|
||||
window.crypto().set_human_key(workspace_id, key)?;
|
||||
Ok(())
|
||||
}
|
||||
98
src-tauri/yaak-crypto/src/encryption.rs
Normal file
98
src-tauri/yaak-crypto/src/encryption.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use crate::error::Error::{DecryptionError, EncryptionError, InvalidEncryptedData};
|
||||
use crate::error::Result;
|
||||
use chacha20poly1305::aead::generic_array::typenum::Unsigned;
|
||||
use chacha20poly1305::aead::{Aead, AeadCore, Key, KeyInit, OsRng};
|
||||
use chacha20poly1305::XChaCha20Poly1305;
|
||||
|
||||
const ENCRYPTION_TAG: &str = "yA4k3nC";
|
||||
const ENCRYPTION_VERSION: u8 = 1;
|
||||
|
||||
pub(crate) fn encrypt_data(data: &[u8], key: &Key<XChaCha20Poly1305>) -> Result<Vec<u8>> {
|
||||
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
|
||||
let cipher = XChaCha20Poly1305::new(&key);
|
||||
let ciphered_data = cipher.encrypt(&nonce, data).map_err(|_| EncryptionError)?;
|
||||
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
data.extend_from_slice(ENCRYPTION_TAG.as_bytes()); // Tag
|
||||
data.push(ENCRYPTION_VERSION); // Version
|
||||
data.extend_from_slice(&nonce.as_slice()); // Nonce
|
||||
data.extend_from_slice(&ciphered_data); // Ciphertext
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt_data(cipher_data: &[u8], key: &Key<XChaCha20Poly1305>) -> Result<Vec<u8>> {
|
||||
// Yaak Tag + ID + Version + Nonce + ... ciphertext ...
|
||||
let (tag, rest) =
|
||||
cipher_data.split_at_checked(ENCRYPTION_TAG.len()).ok_or(InvalidEncryptedData)?;
|
||||
if tag != ENCRYPTION_TAG.as_bytes() {
|
||||
return Err(InvalidEncryptedData);
|
||||
}
|
||||
|
||||
let (version, rest) = rest.split_at_checked(1).ok_or(InvalidEncryptedData)?;
|
||||
if version[0] != ENCRYPTION_VERSION {
|
||||
return Err(InvalidEncryptedData);
|
||||
}
|
||||
|
||||
let nonce_bytes = <XChaCha20Poly1305 as AeadCore>::NonceSize::to_usize();
|
||||
let (nonce, ciphered_data) = rest.split_at_checked(nonce_bytes).ok_or(InvalidEncryptedData)?;
|
||||
|
||||
let cipher = XChaCha20Poly1305::new(&key);
|
||||
cipher.decrypt(nonce.into(), ciphered_data).map_err(|_| DecryptionError)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::encryption::{decrypt_data, encrypt_data};
|
||||
use crate::error::Error::InvalidEncryptedData;
|
||||
use crate::error::Result;
|
||||
use chacha20poly1305::aead::OsRng;
|
||||
use chacha20poly1305::{KeyInit, XChaCha20Poly1305};
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt() -> Result<()> {
|
||||
let key = XChaCha20Poly1305::generate_key(OsRng);
|
||||
let encrypted = encrypt_data("hello world".as_bytes(), &key)?;
|
||||
let decrypted = decrypt_data(encrypted.as_slice(), &key)?;
|
||||
assert_eq!(String::from_utf8(decrypted).unwrap(), "hello world");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_empty() -> Result<()> {
|
||||
let key = XChaCha20Poly1305::generate_key(OsRng);
|
||||
let encrypted = encrypt_data(&[], &key)?;
|
||||
assert_eq!(encrypted.len(), 48);
|
||||
let decrypted = decrypt_data(encrypted.as_slice(), &key)?;
|
||||
assert_eq!(String::from_utf8(decrypted).unwrap(), "");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_bad_version() -> Result<()> {
|
||||
let key = XChaCha20Poly1305::generate_key(OsRng);
|
||||
let mut encrypted = encrypt_data("hello world".as_bytes(), &key)?;
|
||||
encrypted[7] = 0;
|
||||
let decrypted = decrypt_data(encrypted.as_slice(), &key);
|
||||
assert!(matches!(decrypted, Err(InvalidEncryptedData)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_bad_tag() -> Result<()> {
|
||||
let key = XChaCha20Poly1305::generate_key(OsRng);
|
||||
let mut encrypted = encrypt_data("hello world".as_bytes(), &key)?;
|
||||
encrypted[0] = 2;
|
||||
let decrypted = decrypt_data(encrypted.as_slice(), &key);
|
||||
assert!(matches!(decrypted, Err(InvalidEncryptedData)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_unencrypted_data() -> Result<()> {
|
||||
let key = XChaCha20Poly1305::generate_key(OsRng);
|
||||
let decrypted = decrypt_data("123".as_bytes(), &key);
|
||||
assert!(matches!(decrypted, Err(InvalidEncryptedData)));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
47
src-tauri/yaak-crypto/src/error.rs
Normal file
47
src-tauri/yaak-crypto/src/error.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
DbError(#[from] yaak_models::error::Error),
|
||||
|
||||
#[error("Keyring error: {0}")]
|
||||
KeyringError(#[from] keyring::Error),
|
||||
|
||||
#[error("Missing workspace encryption key")]
|
||||
MissingWorkspaceKey,
|
||||
|
||||
#[error("Incorrect workspace key")]
|
||||
IncorrectWorkspaceKey,
|
||||
|
||||
#[error("Crypto IO error: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
|
||||
#[error("Failed to encrypt")]
|
||||
EncryptionError,
|
||||
|
||||
#[error("Failed to decrypt")]
|
||||
DecryptionError,
|
||||
|
||||
#[error("Invalid encrypted data")]
|
||||
InvalidEncryptedData,
|
||||
|
||||
#[error("Invalid key provided")]
|
||||
InvalidHumanKey,
|
||||
|
||||
#[error("Encryption error: {0}")]
|
||||
GenericError(String),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
27
src-tauri/yaak-crypto/src/lib.rs
Normal file
27
src-tauri/yaak-crypto/src/lib.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
extern crate core;
|
||||
|
||||
use crate::commands::*;
|
||||
use crate::manager::EncryptionManager;
|
||||
use tauri::plugin::{Builder, TauriPlugin};
|
||||
use tauri::{generate_handler, Manager, Runtime};
|
||||
|
||||
mod commands;
|
||||
pub mod encryption;
|
||||
pub mod error;
|
||||
pub mod manager;
|
||||
mod master_key;
|
||||
mod workspace_key;
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("yaak-crypto")
|
||||
.invoke_handler(generate_handler![
|
||||
enable_encryption,
|
||||
reveal_workspace_key,
|
||||
set_workspace_key
|
||||
])
|
||||
.setup(|app, _api| {
|
||||
app.manage(EncryptionManager::new(app.app_handle()));
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
171
src-tauri/yaak-crypto/src/manager.rs
Normal file
171
src-tauri/yaak-crypto/src/manager.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use crate::error::Error::{GenericError, IncorrectWorkspaceKey, MissingWorkspaceKey};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::master_key::MasterKey;
|
||||
use crate::workspace_key::WorkspaceKey;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use log::{info, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{AppHandle, Manager, Runtime, State};
|
||||
use yaak_models::models::{EncryptedKey, Workspace, WorkspaceMeta};
|
||||
use yaak_models::query_manager::{QueryManager, QueryManagerExt};
|
||||
use yaak_models::util::{generate_id_of_length, UpdateSource};
|
||||
|
||||
const KEY_USER: &str = "yaak-encryption-key";
|
||||
|
||||
pub trait EncryptionManagerExt<'a, R> {
|
||||
fn crypto(&'a self) -> State<'a, EncryptionManager>;
|
||||
}
|
||||
|
||||
impl<'a, R: Runtime, M: Manager<R>> EncryptionManagerExt<'a, R> for M {
|
||||
fn crypto(&'a self) -> State<'a, EncryptionManager> {
|
||||
self.state::<EncryptionManager>()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EncryptionManager {
|
||||
cached_master_key: Arc<Mutex<Option<MasterKey>>>,
|
||||
cached_workspace_keys: Arc<Mutex<HashMap<String, WorkspaceKey>>>,
|
||||
query_manager: QueryManager,
|
||||
}
|
||||
|
||||
impl EncryptionManager {
|
||||
pub fn new<R: Runtime>(app_handle: &AppHandle<R>) -> Self {
|
||||
Self {
|
||||
cached_master_key: Default::default(),
|
||||
cached_workspace_keys: Default::default(),
|
||||
query_manager: app_handle.db_manager().inner().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt(&self, workspace_id: &str, data: &[u8]) -> Result<Vec<u8>> {
|
||||
let workspace_secret = self.get_workspace_key(workspace_id)?;
|
||||
workspace_secret.encrypt(data)
|
||||
}
|
||||
|
||||
pub fn decrypt(&self, workspace_id: &str, data: &[u8]) -> Result<Vec<u8>> {
|
||||
let workspace_secret = self.get_workspace_key(workspace_id)?;
|
||||
workspace_secret.decrypt(data)
|
||||
}
|
||||
|
||||
pub fn reveal_workspace_key(&self, workspace_id: &str) -> Result<String> {
|
||||
let key = self.get_workspace_key(workspace_id)?;
|
||||
key.to_human()
|
||||
}
|
||||
|
||||
pub fn set_human_key(&self, workspace_id: &str, human_key: &str) -> Result<WorkspaceMeta> {
|
||||
let wkey = WorkspaceKey::from_human(human_key)?;
|
||||
|
||||
let workspace = self.query_manager.connect().get_workspace(workspace_id)?;
|
||||
let encryption_key_challenge = match workspace.encryption_key_challenge {
|
||||
None => return self.set_workspace_key(workspace_id, &wkey),
|
||||
Some(c) => c,
|
||||
};
|
||||
|
||||
let encryption_key_challenge = match BASE64_STANDARD.decode(encryption_key_challenge) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Err(GenericError("Failed to decode workspace challenge".to_string())),
|
||||
};
|
||||
|
||||
if let Err(_) = wkey.decrypt(encryption_key_challenge.as_slice()) {
|
||||
return Err(IncorrectWorkspaceKey);
|
||||
};
|
||||
|
||||
self.set_workspace_key(workspace_id, &wkey)
|
||||
}
|
||||
|
||||
pub(crate) fn set_workspace_key(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
wkey: &WorkspaceKey,
|
||||
) -> Result<WorkspaceMeta> {
|
||||
info!("Created workspace key for {workspace_id}");
|
||||
|
||||
let encrypted_key = BASE64_STANDARD.encode(self.get_master_key()?.encrypt(wkey.raw_key())?);
|
||||
let encrypted_key = EncryptedKey { encrypted_key };
|
||||
let encryption_key_challenge = wkey.encrypt(generate_id_of_length(50).as_bytes())?;
|
||||
let encryption_key_challenge = Some(BASE64_STANDARD.encode(encryption_key_challenge));
|
||||
|
||||
let workspace_meta = self.query_manager.with_tx::<WorkspaceMeta, Error>(|tx| {
|
||||
let workspace = tx.get_workspace(workspace_id)?;
|
||||
let workspace_meta = tx.get_or_create_workspace_meta(workspace_id)?;
|
||||
tx.upsert_workspace(
|
||||
&Workspace {
|
||||
encryption_key_challenge,
|
||||
..workspace
|
||||
},
|
||||
&UpdateSource::Background,
|
||||
)?;
|
||||
|
||||
Ok(tx.upsert_workspace_meta(
|
||||
&WorkspaceMeta {
|
||||
encryption_key: Some(encrypted_key.clone()),
|
||||
..workspace_meta
|
||||
},
|
||||
&UpdateSource::Background,
|
||||
)?)
|
||||
})?;
|
||||
|
||||
let mut cache = self.cached_workspace_keys.lock().unwrap();
|
||||
cache.insert(workspace_id.to_string(), wkey.clone());
|
||||
|
||||
Ok(workspace_meta)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_workspace_key(&self, workspace_id: &str) -> Result<WorkspaceMeta> {
|
||||
let workspace_meta =
|
||||
self.query_manager.connect().get_or_create_workspace_meta(workspace_id)?;
|
||||
|
||||
// Already exists
|
||||
if let Some(_) = workspace_meta.encryption_key {
|
||||
warn!("Tried to create workspace key when one already exists for {workspace_id}");
|
||||
return Ok(workspace_meta);
|
||||
}
|
||||
|
||||
let wkey = WorkspaceKey::create()?;
|
||||
self.set_workspace_key(workspace_id, &wkey)
|
||||
}
|
||||
|
||||
fn get_workspace_key(&self, workspace_id: &str) -> Result<WorkspaceKey> {
|
||||
{
|
||||
let cache = self.cached_workspace_keys.lock().unwrap();
|
||||
if let Some(k) = cache.get(workspace_id) {
|
||||
return Ok(k.clone());
|
||||
}
|
||||
};
|
||||
|
||||
let db = self.query_manager.connect();
|
||||
let workspace_meta = db.get_or_create_workspace_meta(workspace_id)?;
|
||||
|
||||
let key = match workspace_meta.encryption_key {
|
||||
None => return Err(MissingWorkspaceKey),
|
||||
Some(k) => k,
|
||||
};
|
||||
|
||||
let mkey = self.get_master_key()?;
|
||||
let decoded_key = BASE64_STANDARD
|
||||
.decode(key.encrypted_key)
|
||||
.map_err(|e| GenericError(format!("Failed to decode workspace key {e:?}")))?;
|
||||
let raw_key = mkey.decrypt(decoded_key.as_slice())?;
|
||||
info!("Got existing workspace key for {workspace_id}");
|
||||
let wkey = WorkspaceKey::from_raw_key(raw_key.as_slice());
|
||||
|
||||
Ok(wkey)
|
||||
}
|
||||
|
||||
fn get_master_key(&self) -> Result<MasterKey> {
|
||||
{
|
||||
let master_secret = self.cached_master_key.lock().unwrap();
|
||||
if let Some(k) = master_secret.as_ref() {
|
||||
return Ok(k.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
let mkey = MasterKey::get_or_create(KEY_USER)?;
|
||||
let mut master_secret = self.cached_master_key.lock().unwrap();
|
||||
*master_secret = Some(mkey.clone());
|
||||
Ok(mkey)
|
||||
}
|
||||
}
|
||||
78
src-tauri/yaak-crypto/src/master_key.rs
Normal file
78
src-tauri/yaak-crypto/src/master_key.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use crate::encryption::{decrypt_data, encrypt_data};
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use base32::Alphabet;
|
||||
use chacha20poly1305::aead::{Key, KeyInit, OsRng};
|
||||
use chacha20poly1305::XChaCha20Poly1305;
|
||||
use keyring::{Entry, Error};
|
||||
use log::info;
|
||||
|
||||
const HUMAN_PREFIX: &str = "YKM_";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct MasterKey {
|
||||
key: Key<XChaCha20Poly1305>,
|
||||
}
|
||||
|
||||
impl MasterKey {
|
||||
pub(crate) fn get_or_create(user: &str) -> Result<Self> {
|
||||
let entry = Entry::new("app.yaak.desktop.EncryptionKey", user)?;
|
||||
|
||||
let key = match entry.get_password() {
|
||||
Ok(encoded) => {
|
||||
let without_prefix = encoded.strip_prefix(HUMAN_PREFIX).unwrap_or(&encoded);
|
||||
let key_bytes = base32::decode(Alphabet::Crockford {}, &without_prefix)
|
||||
.ok_or(GenericError("Failed to decode master key".to_string()))?;
|
||||
Key::<XChaCha20Poly1305>::clone_from_slice(key_bytes.as_slice())
|
||||
}
|
||||
Err(Error::NoEntry) => {
|
||||
info!("Creating new master key");
|
||||
let key = XChaCha20Poly1305::generate_key(OsRng);
|
||||
let encoded = base32::encode(Alphabet::Crockford {}, key.as_slice());
|
||||
let with_prefix = format!("{HUMAN_PREFIX}{encoded}");
|
||||
entry.set_password(&with_prefix)?;
|
||||
key
|
||||
}
|
||||
Err(e) => return Err(GenericError(e.to_string())),
|
||||
};
|
||||
|
||||
Ok(Self { key })
|
||||
}
|
||||
|
||||
pub(crate) fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
|
||||
encrypt_data(data, &self.key)
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
|
||||
decrypt_data(data, &self.key)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_key() -> Self {
|
||||
let key: Key<XChaCha20Poly1305> = Key::<XChaCha20Poly1305>::clone_from_slice(
|
||||
"00000000000000000000000000000000".as_bytes(),
|
||||
);
|
||||
Self { key }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::error::Result;
|
||||
use crate::master_key::MasterKey;
|
||||
|
||||
#[test]
|
||||
fn test_master_key() -> Result<()> {
|
||||
// Test out the master key
|
||||
let mkey = MasterKey::test_key();
|
||||
let encrypted = mkey.encrypt("hello".as_bytes())?;
|
||||
let decrypted = mkey.decrypt(encrypted.as_slice()).unwrap();
|
||||
assert_eq!(decrypted, "hello".as_bytes().to_vec());
|
||||
|
||||
let mkey = MasterKey::test_key();
|
||||
let decrypted = mkey.decrypt(encrypted.as_slice()).unwrap();
|
||||
assert_eq!(decrypted, "hello".as_bytes().to_vec());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
116
src-tauri/yaak-crypto/src/workspace_key.rs
Normal file
116
src-tauri/yaak-crypto/src/workspace_key.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use crate::encryption::{decrypt_data, encrypt_data};
|
||||
use crate::error::Error::InvalidHumanKey;
|
||||
use crate::error::Result;
|
||||
use base32::Alphabet;
|
||||
use chacha20poly1305::aead::{Key, KeyInit, OsRng};
|
||||
use chacha20poly1305::{KeySizeUser, XChaCha20Poly1305};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkspaceKey {
|
||||
key: Key<XChaCha20Poly1305>,
|
||||
}
|
||||
|
||||
const HUMAN_PREFIX: &str = "YK";
|
||||
|
||||
impl WorkspaceKey {
|
||||
pub(crate) fn to_human(&self) -> Result<String> {
|
||||
let encoded = base32::encode(Alphabet::Crockford {}, self.key.as_slice());
|
||||
let with_prefix = format!("{HUMAN_PREFIX}{encoded}");
|
||||
let with_separators = with_prefix
|
||||
.chars()
|
||||
.collect::<Vec<_>>()
|
||||
.chunks(6)
|
||||
.map(|chunk| chunk.iter().collect::<String>())
|
||||
.collect::<Vec<_>>()
|
||||
.join("-");
|
||||
Ok(with_separators)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn from_human(human_key: &str) -> Result<Self> {
|
||||
let without_prefix = human_key.strip_prefix(HUMAN_PREFIX).unwrap_or(human_key);
|
||||
let without_separators = without_prefix.replace("-", "");
|
||||
let key =
|
||||
base32::decode(Alphabet::Crockford {}, &without_separators).ok_or(InvalidHumanKey)?;
|
||||
if key.len() != XChaCha20Poly1305::key_size() {
|
||||
return Err(InvalidHumanKey);
|
||||
}
|
||||
Ok(Self::from_raw_key(key.as_slice()))
|
||||
}
|
||||
|
||||
pub(crate) fn from_raw_key(key: &[u8]) -> Self {
|
||||
Self {
|
||||
key: Key::<XChaCha20Poly1305>::clone_from_slice(key),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn raw_key(&self) -> &[u8] {
|
||||
self.key.as_slice()
|
||||
}
|
||||
|
||||
pub(crate) fn create() -> Result<Self> {
|
||||
let key = XChaCha20Poly1305::generate_key(OsRng);
|
||||
Ok(Self::from_raw_key(key.as_slice()))
|
||||
}
|
||||
|
||||
pub(crate) fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
|
||||
encrypt_data(data, &self.key)
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
|
||||
decrypt_data(data, &self.key)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_key() -> Self {
|
||||
Self::from_raw_key("f1a2d4b3c8e799af1456be3478a4c3f2".as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::error::Error::InvalidHumanKey;
|
||||
use crate::error::Result;
|
||||
use crate::workspace_key::WorkspaceKey;
|
||||
|
||||
#[test]
|
||||
fn test_persisted_key() -> Result<()> {
|
||||
let key = WorkspaceKey::test_key();
|
||||
let encrypted = key.encrypt("hello".as_bytes())?;
|
||||
assert_eq!(key.decrypt(encrypted.as_slice())?, "hello".as_bytes());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_human_format() -> Result<()> {
|
||||
let key = WorkspaceKey::test_key();
|
||||
|
||||
let encrypted = key.encrypt("hello".as_bytes())?;
|
||||
assert_eq!(key.decrypt(encrypted.as_slice())?, "hello".as_bytes());
|
||||
|
||||
let human = key.to_human()?;
|
||||
assert_eq!(human, "YKCRRP-2CK46H-H36RSR-CMVKJE-B1CRRK-8D9PC9-JK6D1Q-71GK8R-SKCRS0");
|
||||
assert_eq!(
|
||||
WorkspaceKey::from_human(&human)?.decrypt(encrypted.as_slice())?,
|
||||
"hello".as_bytes()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_human_invalid() -> Result<()> {
|
||||
assert!(matches!(
|
||||
WorkspaceKey::from_human(
|
||||
"YKCRRP-2CK46H-H36RSR-CMVKJE-B1CRRK-8D9PC9-JK6D1Q-71GK8R-SKCRS0-H3X38D",
|
||||
),
|
||||
Err(InvalidHumanKey)
|
||||
));
|
||||
|
||||
assert!(matches!(WorkspaceKey::from_human("bad-key",), Err(InvalidHumanKey)));
|
||||
assert!(matches!(WorkspaceKey::from_human("",), Err(InvalidHumanKey)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user