1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
#![doc = include_str!("../README.md")]
//! # Example
//! Retrieving a key from Locky
//! ```rust
//! # use locky::{LockyClient, LockyEnv};
//! # use aes_gcm::{
//! # aead::{Aead, AeadCore, KeyInit, OsRng},
//! # Aes256Gcm, Nonce, Key
//! # };
//! # tokio_test::block_on(async {
//! # // make an account for testing
//! # let (account_id, access_token) = locky::get_test_account().await;
//! // Connect to Locky staging environment.
//! let mut client = LockyClient::new(LockyEnv::Staging)
//! .with_creds(account_id, access_token);
//! # client.create_key("test_db_key").await.unwrap();
//!
//! // Securely get a secret from the cloud service
//! let key = client.get_key("test_db_key").await.unwrap();
//!
//! // Use the secret to encrypt some data
//! let cipher = Aes256Gcm::new((&*key).into());
//!
//! // Never send this key over a network. Even if the communication is encrypted,
//! // unless it specifially uses a post-quantum secure protocol (such as the one
//! // one used by Locky) it will vulnerable to harvest-now decrypt-later
//! // attacks.
//! drop(key);
//! # });
//! ```
//!
//! ## Creating an account
//! ```rust
//! # use locky::{LockyClient, LockyEnv};
//! # tokio_test::block_on(async {
//! let mut client = LockyClient::new(LockyEnv::Staging);
//!
//! // Make an account in our staging environment
//! let account_id = client.create_account("cool-test-account@getloc.ky").await.unwrap();
//!
//! // the access token needs to be stored securely, but it does not need
//! // to be stored in a quantum-secure manner. So however you currently
//! // manage secrets is probably fine!
//! let access_token = client.get_access_token().unwrap();
//! # });
//! ```
//!
//! ## Creating a key
//! ```rust
//! # use locky::{LockyClient, LockyEnv};
//! # tokio_test::block_on(async {
//! # let (account_id, access_token) = locky::get_test_account().await;
//! let mut client = LockyClient::new(LockyEnv::Staging).with_creds(account_id, access_token);
//!
//! // Alternately, you can use our CLI or web interface to create a key
//! client.create_key("test_key").await.unwrap();
//! # });
//! ```
//!
//! # A Note On Staging
//! The staging environment is **deleted every 24 hours**. It is a test environment.
//! Security is not guaranteed and any accounts, keys, or data you create
//! will be lost. Do not store anything in staging besides ephemeral test data!
#![doc(html_logo_url = "https://github.githubassets.com/images/icons/emoji/unicode/1f9e9.png")]
#![doc(html_favicon_url = "https://github.githubassets.com/images/icons/emoji/unicode/1f9e9.png")]
mod autogenerated;
use aes_kw::KekAes256;
use autogenerated::api::locky_client::LockyClient as GrpcLockyClient;
use autogenerated::api::{CreateAccountRequest, CreateKeyRequest, GetKeyRequest};
use ml_kem_rs::ml_kem_768;
use ml_kem_rs::ml_kem_768::{CipherText, DecapsKey};
use tonic::transport::{Channel, ClientTlsConfig};
use zeroize::{Zeroize, Zeroizing};
/// LockyClient is a client for interacting with the Locky service.
///
/// It provides methods for creating an account, managing credentials, creating keys, and retrieving keys.
/// The client can be configured with different environments, such as staging or production.
pub struct LockyClient {
client: Option<GrpcLockyClient<Channel>>,
creds: Option<(String, String)>,
env: LockyEnv,
}
pub enum LockyEnv {
Staging,
Production,
}
impl LockyClient {
pub fn new(env: LockyEnv) -> Self {
LockyClient {
client: None,
creds: None,
env,
}
}
pub fn with_creds<S>(self, account_id: S, access_token: S) -> Self
where
S: Into<String>,
{
LockyClient {
creds: Some((account_id.into(), access_token.into())),
..self
}
}
pub async fn create_account<S>(
&mut self,
email: S,
) -> Result<String, Box<dyn std::error::Error>>
where
S: Into<String>,
{
match self.creds.as_ref() {
Some(_) => {
return Err("already logged in! use a new LockyClient to make a new account".into())
}
None => {
let request = tonic::Request::new(CreateAccountRequest {
email: email.into(),
});
let response = self
.get_client()
.await?
.create_account(request)
.await?
.into_inner();
self.creds = Some((response.account_id.clone(), response.access_token));
Ok(response.account_id)
}
}
}
pub fn get_access_token(&self) -> Result<String, Box<dyn std::error::Error>> {
match self.creds.as_ref() {
None => Err("must create_account or use with_creds to provide credentials".into()),
Some((_, access_token)) => Ok(access_token.clone()),
}
}
pub async fn create_key<S>(&mut self, name: S) -> Result<(), Box<dyn std::error::Error>>
where
S: Into<String>,
{
match self.creds.as_ref() {
None => {
return Err("must create_account or use with_creds to provide credentials".into())
}
Some((account_id, access_token)) => {
let request = tonic::Request::new(CreateKeyRequest {
account_id: account_id.clone(),
access_token: access_token.clone(),
name: name.into(),
});
self.get_client().await?.create_key(request).await?;
Ok(())
}
}
}
pub async fn get_key<S>(
&mut self,
name: S,
) -> Result<Zeroizing<[u8; 32]>, Box<dyn std::error::Error>>
where
S: Into<String>,
{
match self.creds.as_ref() {
None => {
return Err("must create_account or use with_creds to provide credentials".into())
}
Some((account_id, access_token)) => {
let (ek, dk) = ml_kem_768::key_gen();
let request = tonic::Request::new(GetKeyRequest {
account_id: account_id.clone(),
access_token: access_token.clone(),
name: name.into(),
ephemeral_encaps_key: ek.to_bytes().to_vec(),
});
let response = self
.get_client()
.await?
.get_key(request)
.await?
.into_inner();
let ct = ml_kem_768::new_ct(
response
.encaps_ciphertext
.try_into()
.map_err(|_| "bad encaps_ciphertext")?,
);
let key = decrypt_key(
&dk,
&ct,
response
.key_ciphertext
.try_into()
.map_err(|_| "bad key_ciphertext")?,
)?;
Ok(key)
}
}
}
async fn get_client(
&mut self,
) -> Result<&mut GrpcLockyClient<Channel>, Box<dyn std::error::Error>> {
if self.client.is_none() {
match self.env {
LockyEnv::Staging => Ok(self.client.insert({
GrpcLockyClient::new(
Channel::from_static("https://api.staging.getloc.ky:443")
.tls_config(
ClientTlsConfig::new().domain_name("api.staging.getloc.ky"),
)?
.connect()
.await?,
)
})),
LockyEnv::Production => {
unimplemented!("Locky production environment not yet supported");
}
}
} else {
Ok(self.client.as_mut().unwrap())
}
}
}
fn decrypt_key(
dk: &DecapsKey,
ct: &CipherText,
mut to_dec: [u8; 40],
) -> Result<Zeroizing<[u8; 32]>, Box<dyn std::error::Error>> {
let ssk = dk.decaps(&ct);
let kek = KekAes256::from(ssk.to_bytes());
let mut res = Zeroizing::new([0u8; 32]);
kek.unwrap(&to_dec, res.as_mut())
.map_err(|_| "failed to decrypt key from Locky")?;
to_dec.zeroize();
Ok(res)
}
#[doc(hidden)]
pub async fn get_test_account() -> (String, String) {
let mut client = LockyClient::new(LockyEnv::Staging);
let account_id = client
.create_account("doctest-acct@getloc.ky")
.await
.unwrap();
(account_id, client.get_access_token().unwrap())
}