Skip to content

Commit

Permalink
feat: implement setting encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
zensh committed Jul 22, 2024
1 parent a0bb46a commit 201be55
Show file tree
Hide file tree
Showing 10 changed files with 346 additions and 88 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ crc32fast = "1.4"
url = "2.5"
once_cell = "1.19"
getrandom = { version = "0.2", features = ["custom"] }
coset = { git = "https://github.com/ldclabs/coset.git", rev = "50583c5df52b653eb9b17c2940ad4b7fe3d67ff1" }
aes-gcm = "0.10"
2 changes: 1 addition & 1 deletion src/ic_cose_canister/src/api_query.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use ic_cose_types::{
crypto::*,
cose::PublicKeyInput,
namespace::NamespaceInfo,
setting::{SettingInfo, SettingPath},
state::StateInfo,
Expand Down
78 changes: 65 additions & 13 deletions src/ic_cose_canister/src/api_update.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use ic_cose_types::{crypto::*, setting::*, MILLISECONDS};
use ic_cose_types::{
cose::*, crypto::ecdh_x25519, format_error, mac3_256, setting::*, sha3_256, MILLISECONDS,
};
use serde_bytes::ByteBuf;

use crate::{rand_bytes, store};
Expand Down Expand Up @@ -43,23 +45,27 @@ async fn get_setting(path: SettingPath, ecdh: Option<ECDHInput>) -> Result<Setti
info.payload = Some(payload);
Ok(info)
} else {
Err("missing ECDH for encrypted paylod".to_string())
Err("missing ECDH for payload encryption".to_string())
}
}
Some(ecdh) => {
let aad = spk.2.as_slice();
let data = match dek {
None => payload,
Some(dek) => {
let partial_key = ecdh.partial_key.ok_or("missing partial key")?;
let key = store::ns::ecdsa_setting_kek(&spk, partial_key.as_ref()).await?;
let key = cose_decrypt0(&dek, &key, aad)?;
let key = CoseKey::from_slice(&key).map_err(format_error)?;
let dek = cose_key_secret(key)?;
cose_decrypt0(&payload, &dek, aad)?
}
};

let secret_key = rand_bytes().await;
let secret_key = mac3_256(&secret_key, ecdh.nonce.as_ref());
let (secret_key, public_key) = ecdh_x25519(secret_key, *ecdh.public_key);

let kek = if dek.is_some() {
let partial_key = ecdh.partial_key.ok_or("missing partial key")?;
let key = store::ns::ecdsa_setting_kek(&spk, partial_key.as_ref()).await?;
Some(key)
} else {
None
};

let payload = cose_re_encrypt(secret_key.to_bytes(), payload, kek, dek).await?;
let payload = cose_encrypt0(&data, secret_key.as_bytes(), aad, *ecdh.nonce)?;
info.payload = Some(payload);
info.public_key = Some(ByteBuf::from(public_key.to_bytes()));
Ok(info)
Expand Down Expand Up @@ -118,5 +124,51 @@ async fn update_setting_payload(
let caller = ic_cdk::caller();
let subject = path.subject.unwrap_or(caller);
let spk = store::SettingPathKey::from_path(path, subject);
Err("not implemented".to_string())
let (info, dek, max_payload_size) = store::ns::setting_for_update_payload(&caller, &spk)?;
if info.version != input.version {
Err("version mismatch".to_string())?;
}

if dek.is_some() && ecdh.is_none() {
Err("missing ECDH for payload encryption".to_string())?;
}

let now_ms = ic_cdk::api::time() / MILLISECONDS;
match ecdh {
None => {
let _ = try_decode_payload(max_payload_size, info.ctype, &input.payload)?;
let hash = sha3_256(&input.payload);
store::ns::update_setting_payload(
&spk,
input.payload,
None,
hash.into(),
input.status,
now_ms,
)
}
Some(ecdh) => {
let partial_key = ecdh.partial_key.ok_or("missing partial key")?;
let (secret_key, _) = store::ns::ecdh_x25519_static_secret(&spk, &ecdh).await?;
let aad = spk.2.as_slice();
let data = cose_decrypt0(&input.payload, secret_key.as_bytes(), aad)?;
let _ = try_decode_payload(max_payload_size, info.ctype, &data)?;
let hash = sha3_256(&data);
let dek = rand_bytes().await;
let dek = mac3_256(&dek, ecdh.nonce.as_ref());
let payload = cose_encrypt0(&data, &dek, aad, *ecdh.nonce)?;
let key = cose_aes256_key(dek);
let key = key.to_vec().map_err(format_error)?;
let kek = store::ns::ecdsa_setting_kek(&spk, partial_key.as_ref()).await?;
let dek = cose_encrypt0(&key, &kek, aad, *ecdh.nonce)?;
store::ns::update_setting_payload(
&spk,
payload,
Some(dek),
hash.into(),
input.status,
now_ms,
)
}
}
}
2 changes: 1 addition & 1 deletion src/ic_cose_canister/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use candid::Principal;
use ic_cose_types::{crypto::*, namespace::*, setting::*, state::StateInfo};
use ic_cose_types::{cose::*, namespace::*, setting::*, state::StateInfo};
use serde_bytes::ByteBuf;
use std::collections::BTreeSet;

Expand Down
106 changes: 92 additions & 14 deletions src/ic_cose_canister/src/store.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
use candid::Principal;
use ciborium::{from_reader, from_reader_with_buffer, into_writer};
use ic_cose_types::{
crypto::{ecdh_x25519, sha3_256, sha3_256_n, ECDHInput},
namespace::NamespaceInfo,
setting::*,
state::StateInfo,
ByteN,
cose::ECDHInput, crypto::ecdh_x25519, namespace::NamespaceInfo, setting::*, sha3_256,
sha3_256_n, state::StateInfo, ByteN,
};
use ic_stable_structures::{
memory_manager::{MemoryId, MemoryManager, VirtualMemory},
Expand Down Expand Up @@ -154,7 +151,7 @@ impl Namespace {
None
}

pub fn get_setting(
pub fn check_and_get_setting(
&self,
caller: &Principal,
setting_path: &SettingPathKey,
Expand All @@ -176,6 +173,15 @@ impl Namespace {
})
}

pub fn get_setting(&self, setting_path: &SettingPathKey) -> Option<&Setting> {
let setting_key = (setting_path.2, setting_path.3.clone());
match setting_path.1 {
0 => self.settings.get(&setting_key),
1 => self.client_settings.get(&setting_key),
_ => None,
}
}

pub fn get_setting_mut(&mut self, setting_path: &SettingPathKey) -> Option<&mut Setting> {
let setting_key = (setting_path.2, setting_path.3.clone());
match setting_path.1 {
Expand Down Expand Up @@ -388,6 +394,7 @@ pub mod state {
}

pub mod ns {

use ic_cose_types::setting::CreateSettingInput;

use super::*;
Expand Down Expand Up @@ -512,7 +519,7 @@ pub mod ns {
.ok_or_else(|| format!("namespace {} not found", setting_path.0))?;

let value = ns
.get_setting(caller, setting_path)
.check_and_get_setting(caller, setting_path)
.ok_or_else(|| format!("setting {} not found or no permission", setting_path))?;

Ok(value.to_info(setting_path.2, setting_path.3.clone()))
Expand All @@ -529,15 +536,15 @@ pub mod ns {
.get(&setting_path.0)
.ok_or_else(|| format!("namespace {} not found", setting_path.0))?;

let value = ns
.get_setting(caller, setting_path)
let setting = ns
.check_and_get_setting(caller, setting_path)
.ok_or_else(|| format!("setting {} not found or no permission", setting_path))?;

if setting_path.4 > 0 && setting_path.4 < value.version {
if setting_path.4 > 0 && setting_path.4 < setting.version {
Err("setting version not found".to_string())?;
}

let payload = if setting_path.4 < value.version {
let payload = if setting_path.4 < setting.version {
PAYLOADS_STORE.with(|r| {
let m = r.borrow();
let payload = m
Expand All @@ -546,17 +553,17 @@ pub mod ns {
Ok::<ByteBuf, String>(ByteBuf::from(payload))
})?
} else {
value
setting
.payload
.as_ref()
.ok_or_else(|| format!("setting {} payload not found", setting_path))?
.clone()
};

Ok((
value.to_info(setting_path.2, setting_path.3.clone()),
setting.to_info(setting_path.2, setting_path.3.clone()),
payload,
value.dek.clone(),
setting.dek.clone(),
))
})
}
Expand Down Expand Up @@ -632,6 +639,9 @@ pub mod ns {
let setting = ns
.get_setting_mut(setting_path)
.ok_or_else(|| format!("setting {} not found", setting_path))?;
if setting.status == 1 {
Err("readonly setting can not be updated".to_string())?;
}

if let Some(status) = input.status {
if status == 1 && setting.payload.is_none() {
Expand All @@ -654,4 +664,72 @@ pub mod ns {
})
})
}

pub fn setting_for_update_payload(
caller: &Principal,
setting_path: &SettingPathKey,
) -> Result<(SettingInfo, Option<ByteBuf>, u64), String> {
NS.with(|r| {
let m = r.borrow();
let ns = m
.get(&setting_path.0)
.ok_or_else(|| format!("namespace {} not found", setting_path.0))?;

if !ns.can_write_setting(caller, setting_path) {
Err("no permission".to_string())?;
}
let setting = ns
.get_setting(setting_path)
.ok_or_else(|| format!("setting {} not found", setting_path))?;
if setting.status == 1 {
Err("readonly setting can not be updated".to_string())?;
}

Ok((
setting.to_info(setting_path.2, setting_path.3.clone()),
setting.dek.clone(),
ns.max_payload_size,
))
})
}

pub fn update_setting_payload(
setting_path: &SettingPathKey,
payload: ByteBuf,
dek: Option<ByteBuf>,
hash: ByteN<32>,
status: Option<i8>,
now_ms: u64,
) -> Result<UpdateSettingOutput, String> {
NS.with(|r| {
let mut m = r.borrow_mut();
let ns = m
.get_mut(&setting_path.0)
.ok_or_else(|| format!("namespace {} not found", setting_path.0))?;

let setting = ns
.get_setting_mut(setting_path)
.ok_or_else(|| format!("setting {} not found", setting_path))?;
if setting.status == 1 {
Err("readonly setting can not be updated".to_string())?;
}

setting.version = setting.version.saturating_add(1);
setting.hash = Some(hash);
setting.payload = Some(payload);
setting.dek = dek;
setting.updated_at = now_ms;
if let Some(status) = status {
setting.status = status;
}

Ok(UpdateSettingOutput {
created_at: setting.created_at,
updated_at: setting.updated_at,
version: setting.version,
hash: Some(hash),
public_key: None,
})
})
}
}
2 changes: 2 additions & 0 deletions src/ic_cose_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ x25519-dalek = { workspace = true }
hmac = { workspace = true }
sha2 = { workspace = true }
sha3 = { workspace = true }
coset = { workspace = true }
aes-gcm = { workspace = true }
Loading

0 comments on commit 201be55

Please sign in to comment.