Skip to main content
Glama
secret.rs37.9 kB
//! This module contains [`Secret`], which is a reference to an underlying [`EncryptedSecret`]. #![warn( bad_style, clippy::missing_panics_doc, clippy::panic, clippy::panic_in_result_fn, clippy::unwrap_in_result, clippy::unwrap_used, dead_code, improper_ctypes, missing_debug_implementations, no_mangle_generic_items, non_shorthand_field_patterns, overflowing_literals, path_statements, patterns_in_fns_without_body, unconditional_recursion, unreachable_pub, unused, unused_allocation, unused_comparisons, unused_parens, while_true )] use std::{ collections::HashMap, fmt, str::FromStr, sync::Arc, }; use chrono::Utc; use serde::{ Deserialize, Serialize, }; use serde_json::Value; use si_crypto::{ SymmetricCryptoError, SymmetricCryptoService, SymmetricNonce, }; use si_data_pg::PgError; use si_db::HistoryActor; use si_events::{ ContentHash, EncryptedSecretKey, Timestamp, encrypted_secret::EncryptedSecretKeyParseError, ulid::Ulid, }; use si_hash::Hash; use si_id::{ PropId, SchemaVariantId, }; use si_layer_cache::LayerDbError; use sodiumoxide::crypto::{ box_::{ PublicKey, SecretKey, }, sealedbox, }; use telemetry::prelude::*; use thiserror::Error; use veritech_client::SensitiveContainer; use crate::{ AttributePrototype, AttributeValue, AttributeValueId, ChangeSetError, ComponentError, ComponentId, DalContext, Func, FuncError, FuncId, HelperError, KeyPair, KeyPairError, Prop, SchemaVariant, SchemaVariantError, TransactionsError, UserPk, attribute::{ prototype::{ AttributePrototypeError, argument::{ AttributePrototypeArgument, AttributePrototypeArgumentError, }, }, value::AttributeValueError, }, func::{ argument::{ FuncArgument, FuncArgumentError, }, intrinsics::IntrinsicFunc, }, implement_add_edge_to, key_pair::KeyPairPk, layer_db_types::{ SecretContent, SecretContentV1, }, prop::PropError, schema::variant::root_prop::RootPropChild, serde_impls::{ base64_bytes_serde, nonce_serde, }, workspace_snapshot::{ WorkspaceSnapshotError, dependent_value_root::DependentValueRootError, edge_weight::{ EdgeWeightKind, EdgeWeightKindDiscriminants, }, node_weight::{ NodeWeight, NodeWeightError, category_node_weight::CategoryNodeKind, secret_node_weight::SecretNodeWeight, }, }, }; mod algorithm; mod definition_view; mod event; mod view; pub use algorithm::{ SecretAlgorithm, SecretVersion, }; pub use definition_view::{ SecretDefinitionView, SecretDefinitionViewError, }; pub use event::{ SecretCreatedPayload, SecretDeletedPayload, SecretUpdatedPayload, }; pub use view::{ SecretView, SecretViewError, SecretViewResult, }; #[allow(missing_docs)] #[remain::sorted] #[derive(Error, Debug)] pub enum SecretError { #[error("attribute prototype error: {0}")] AttributePrototype(#[from] Box<AttributePrototypeError>), #[error("attribute prototype argument error: {0}")] AttributePrototypeArgument(#[from] Box<AttributePrototypeArgumentError>), #[error("attribute value error: {0}")] AttributeValue(#[from] Box<AttributeValueError>), #[error("change set error: {0}")] ChangeSet(#[from] ChangeSetError), #[error("component error: {0}")] Component(#[from] Box<ComponentError>), #[error("error when decrypting encrypted secret")] DecryptionFailed, #[error("dependent value root error: {0}")] DependentValueRoot(#[from] DependentValueRootError), #[error("error deserializing message: {0}")] DeserializeMessage(#[source] serde_json::Error), #[error("encrypted secret key parse error: {0}")] EncryptedSecretKeyParse(#[from] EncryptedSecretKeyParseError), #[error("encrypted secret not found for key: {0}")] EncryptedSecretNotFound(EncryptedSecretKey), #[error("func error: {0}")] Func(#[from] Box<FuncError>), #[error("func argument error: {0}")] FuncArgument(#[from] Box<FuncArgumentError>), #[error("func argument not found for func ({0}) and name ({1})")] FuncArgumentNotFound(FuncId, String), #[error("helper error: {0}")] Helper(#[from] HelperError), #[error("key pair error: {0}")] KeyPair(#[from] KeyPairError), #[error("key pair not found for secret")] KeyPairNotFound, #[error("layer db error: {0}")] LayerDb(#[from] LayerDbError), #[error("node weight error: {0}")] NodeWeight(#[from] NodeWeightError), #[error("secret not found for key: [redacted]")] NotFoundForKey, #[error("pg error: {0}")] Pg(#[from] PgError), #[error("prop error: {0}")] Prop(#[from] Box<PropError>), #[error("not an error! prop id is not for a secret during MV build: {0}")] PropIdNotForSecret(PropId), #[error("schema variant error: {0}")] SchemaVariant(#[from] Box<SchemaVariantError>), #[error("schema variant not secret defining: {0}")] SchemaVariantNotSecretDefining(SchemaVariantId), #[error("secret not found: {0}")] SecretNotFound(SecretId), #[error("serde json error: {0}")] SerdeJson(#[from] serde_json::Error), #[error("symmetric crypto error: {0}")] SymmetricCrypto(#[from] SymmetricCryptoError), #[error("transactions error: {0}")] Transactions(#[from] TransactionsError), #[error("workspace snapshot error: {0}")] WorkspaceSnapshot(#[from] WorkspaceSnapshotError), } #[allow(missing_docs)] pub type SecretResult<T> = Result<T, SecretError>; pub use si_id::SecretId; /// A reference to an [`EncryptedSecret`] with metadata. /// /// This type does not contain any encrypted information nor any encryption metadata and is /// therefore safe to expose via external API. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Secret { id: SecretId, encrypted_secret_key: EncryptedSecretKey, #[serde(flatten)] timestamp: Timestamp, created_by: Option<UserPk>, updated_by: Option<UserPk>, name: String, definition: String, description: Option<String>, } impl From<Secret> for SecretContentV1 { fn from(value: Secret) -> Self { Self { timestamp: value.timestamp, created_by: value.created_by, updated_by: value.updated_by, name: value.name, definition: value.definition, description: value.description, } } } impl Secret { #[allow(missing_docs)] pub fn assemble(secret_node_weight: SecretNodeWeight, content: SecretContentV1) -> Self { Self { id: secret_node_weight.id().into(), encrypted_secret_key: secret_node_weight.encrypted_secret_key().to_owned(), timestamp: content.timestamp, created_by: content.created_by, updated_by: content.updated_by, name: content.name, definition: content.definition, description: content.description, } } implement_add_edge_to!( source_id: Ulid, destination_id: SecretId, add_fn: add_category_edge, discriminant: EdgeWeightKindDiscriminants::Use, result: SecretResult, ); /// Creates a new [`Secret`] with a corresponding [`EncryptedSecret`]. #[allow(clippy::too_many_arguments)] pub async fn new( ctx: &DalContext, name: impl Into<String>, definition: impl Into<String>, description: Option<String>, crypted: &[u8], key_pair_pk: KeyPairPk, version: SecretVersion, algorithm: SecretAlgorithm, ) -> SecretResult<Self> { let user = match ctx.history_actor() { HistoryActor::SystemInit => None, HistoryActor::User(user_pk) => Some(*user_pk), }; let id = ctx.workspace_snapshot()?.generate_ulid().await?; let lineage_id = ctx.workspace_snapshot()?.generate_ulid().await?; let secret_id = id.into(); // Generate a key for the underlying encrypted secret. let key = Self::generate_key(ctx, secret_id).await?; let content = SecretContentV1 { timestamp: Timestamp::now(), created_by: user, updated_by: user, name: name.into(), definition: definition.into(), description, }; let (hash, _) = ctx.layer_db().cas().write( Arc::new(SecretContent::V1(content.clone()).into()), None, ctx.events_tenancy(), ctx.events_actor(), )?; let node_weight = NodeWeight::new_secret(id, lineage_id, key, hash); let secret_node_weight = node_weight.get_secret_node_weight()?; let workspace_snapshot = ctx.workspace_snapshot()?; workspace_snapshot.add_or_replace_node(node_weight).await?; // Root --> Secret Category --> Secret (this) let secret_category_id = workspace_snapshot .get_category_node_or_err(CategoryNodeKind::Secret) .await?; Self::add_category_edge( ctx, secret_category_id, id.into(), EdgeWeightKind::new_use(), ) .await?; let secret = Self::assemble(secret_node_weight, content); // After creating the secret on the graph, create an underlying encrypted secret and use // the key we assembled. EncryptedSecret::insert(ctx, key, crypted, key_pair_pk, version, algorithm).await?; Ok(secret) } /// Generates a key based on the [`Tenancy`](crate::Tenancy), [`SecretId`] and a newly generated /// [`Ulid`]. /// /// A new key should be assembled anytime an [`EncryptedSecret`] is created or mutated. This /// method is purposefully owned by [`Secret`] to help ensure that we don't generate a key based /// on any encrypted contents or parameters to insert encrypted contents. async fn generate_key( ctx: &DalContext, secret_id: SecretId, ) -> SecretResult<EncryptedSecretKey> { let new_ulid = ctx.workspace_snapshot()?.generate_ulid().await?; let mut hasher = EncryptedSecretKey::hasher(); hasher.update(&ctx.tenancy().to_bytes()); hasher.update(secret_id.to_string().as_bytes()); hasher.update(new_ulid.to_string().as_bytes()); Ok(hasher.finalize()) } /// Returns the [`id`](SecretId). pub fn id(&self) -> SecretId { self.id } /// Returns a reference to the name. pub fn name(&self) -> &str { self.name.as_ref() } /// Returns a reference to the definition. pub fn definition(&self) -> &str { self.definition.as_ref() } /// Returns a reference to the description. pub fn description(&self) -> &Option<String> { &self.description } /// Returns the key corresponding to the corresponding [`EncryptedSecret`]. pub fn encrypted_secret_key(&self) -> EncryptedSecretKey { self.encrypted_secret_key } /// Returns if the secret can be decrypted in this workspace pub async fn can_be_decrypted(&self, ctx: &DalContext) -> SecretResult<bool> { let key = self.encrypted_secret_key; let Some(encrypted_secret) = EncryptedSecret::get_by_key(ctx, key).await? else { return Ok(false); }; match encrypted_secret.key_pair(ctx).await { Ok(_) => Ok(true), Err(SecretError::KeyPair(KeyPairError::KeyPairNotFound(_))) | Err(SecretError::KeyPair(KeyPairError::UnauthorizedKeyAccess)) => Ok(false), Err(err) => Err(err), } } /// Attach a [`Secret`] to a given [`AttributeValue`] corresponding to a /// "/root/secrets/\<secret\>" [`Prop`](crate::Prop). /// /// - If a [`Secret`] has already been attached to the given [`AttributeValue`], the existing /// attachment will be replaced with a new one. /// - If no [`Secret`] is provided, then the [`AttributeValue`] will be updated to its original /// state via [`AttributeValue::use_default_prototype`]. /// /// This method will enqueue /// [`DependentValuesUpdate`](crate::job::definition::DependentValuesUpdate). pub async fn attach_for_attribute_value( ctx: &DalContext, attribute_value_id: AttributeValueId, secret_id: Option<SecretId>, ) -> SecretResult<()> { // First, check if the caller would like to unset the secret. let secret_id = match secret_id { Some(provided_secret_id) => provided_secret_id, None => { // We use the default prototype here rather than passing "None" to // "AttributeValue::update" because we neither want nor need existing prototype // arguments and the user cannot override the prototype for values corresponding // to a given "/root/secrets/<secret>". AttributeValue::use_default_prototype(ctx, attribute_value_id).await?; return Ok(()); } }; // Cache the variables we need for creating the attachment. let func_argument_name = "identity"; let func_id = Func::find_intrinsic(ctx, IntrinsicFunc::Identity).await?; // Create a new prototype and replace the existing one. If we are updating which secret is // used for the provided attribute value, this will remove the attribute prototype argument // that uses the previous secret. Despite having the ability to update the attribute // prototype argument value source, we need to re-create the prototype every time this // method is called. Why? We cannot be certain that the existing prototype and its arguments // (0 to N) will be what we need. let attribute_prototype = AttributePrototype::new(ctx, func_id).await?; AttributeValue::set_component_prototype_id( ctx, attribute_value_id, attribute_prototype.id(), None, ) .await?; // Create an attribute prototype argument whose value source is the secret provided. Then, // enqueue dependent values update with the secret. let func_argument = FuncArgument::find_by_name_for_func(ctx, func_argument_name, func_id) .await? .ok_or(SecretError::FuncArgumentNotFound( func_id, func_argument_name.to_owned(), ))?; AttributePrototypeArgument::new(ctx, attribute_prototype.id(), func_argument.id, secret_id) .await?; ctx.add_dependent_values_and_enqueue(vec![secret_id]) .await?; Ok(()) } /// Gets the [`Secret`] with a given [`SecretId`]. If a [`Secret`] is not found, then return an /// [`error`](SecretError). /// /// _Note:_ this does not contain the encrypted or sensitive bits and is safe for external use. pub async fn get_by_id(ctx: &DalContext, id: SecretId) -> SecretResult<Self> { let (secret_node_weight, hash) = Self::get_node_weight_and_content_hash_or_error(ctx, id).await?; let content: SecretContent = ctx .layer_db() .cas() .try_read_as(&hash) .await? .ok_or(WorkspaceSnapshotError::MissingContentFromStore(id.into()))?; // NOTE(nick): if we had a v2, then there would be migration logic here. let SecretContent::V1(inner) = content; Ok(Self::assemble(secret_node_weight, inner)) } async fn get_node_weight_and_content_hash_or_error( ctx: &DalContext, id: SecretId, ) -> SecretResult<(SecretNodeWeight, ContentHash)> { let workspace_snapshot = ctx.workspace_snapshot()?; let node_weight = workspace_snapshot.get_node_weight(id).await?; let hash = node_weight.content_hash(); let secret_node_weight = node_weight.get_secret_node_weight()?; Ok((secret_node_weight, hash)) } /// Prepares the serialized payload for prototype execution for a given [`SecretId`](Secret). /// /// The corresponding [`EncryptedSecretKey`] will be converted to a string and then serialized /// as JSON. pub async fn payload_for_prototype_execution( ctx: &DalContext, secret_id: SecretId, ) -> SecretResult<Value> { let secret = Self::get_by_id(ctx, secret_id).await?; Ok(serde_json::to_value( secret.encrypted_secret_key.to_string(), )?) } /// Deserializes the value contained in an [`AttributeValue`] corresponding to a secret. The /// value will be a string, which then needs to be parsed to get the [`EncryptedSecretKey`]. pub fn key_from_value_in_attribute_value(value: Value) -> SecretResult<EncryptedSecretKey> { let deserialized: String = serde_json::from_value(value)?; let key = EncryptedSecretKey::from_str(&deserialized)?; Ok(key) } /// Find all [`AttributeValues`](AttributeValue) that _directly_ depend on the [`Secret`] /// corresponding to the provided [`SecretId`](Secret). pub async fn direct_dependent_attribute_values( ctx: &DalContext, secret_id: SecretId, ) -> SecretResult<Vec<AttributeValueId>> { let workspace_snapshot = ctx.workspace_snapshot()?; let attribute_prototype_argument_indices = workspace_snapshot .incoming_sources_for_edge_weight_kind( secret_id, EdgeWeightKindDiscriminants::PrototypeArgumentValue, ) .await?; let mut attribute_value_ids = Vec::with_capacity(attribute_prototype_argument_indices.len()); for attribute_prototype_argument_index in attribute_prototype_argument_indices { let attribute_prototype_argument_node_weight = workspace_snapshot .get_node_weight(attribute_prototype_argument_index) .await? .get_attribute_prototype_argument_node_weight()?; let attribute_prototype_id = AttributePrototypeArgument::prototype_id( ctx, attribute_prototype_argument_node_weight.id().into(), ) .await?; attribute_value_ids.extend( AttributePrototype::attribute_value_ids(ctx, attribute_prototype_id).await?, ); } Ok(attribute_value_ids) } /// Assemble an object that maps all [`keys`](EncryptedSecretKey) to their corresponding /// [`SecretIds`](Secret). #[instrument(level = "debug", skip(ctx))] pub async fn list_ids_by_key( ctx: &DalContext, ) -> SecretResult<HashMap<EncryptedSecretKey, SecretId>> { let workspace_snapshot = ctx.workspace_snapshot()?; let secret_category_node_id = workspace_snapshot .get_category_node_or_err(CategoryNodeKind::Secret) .await?; let indices = workspace_snapshot .outgoing_targets_for_edge_weight_kind( secret_category_node_id, EdgeWeightKindDiscriminants::Use, ) .await?; let mut id_by_key = HashMap::new(); for index in indices { let secret_node_weight = workspace_snapshot .get_node_weight(index) .await? .get_secret_node_weight()?; id_by_key.insert( secret_node_weight.encrypted_secret_key().to_owned(), secret_node_weight.id().into(), ); } Ok(id_by_key) } /// Get the [`SecretId`] corresponding to a given [`key`](EncryptedSecretKey). #[instrument(level = "debug", skip(ctx))] pub async fn get_id_by_key_or_error( ctx: &DalContext, key: EncryptedSecretKey, ) -> SecretResult<SecretId> { let workspace_snapshot = ctx.workspace_snapshot()?; let secret_category_node_id = workspace_snapshot .get_category_node_or_err(CategoryNodeKind::Secret) .await?; let indices = workspace_snapshot .outgoing_targets_for_edge_weight_kind( secret_category_node_id, EdgeWeightKindDiscriminants::Use, ) .await?; for index in indices { let secret_node_weight = workspace_snapshot .get_node_weight(index) .await? .get_secret_node_weight()?; if secret_node_weight.encrypted_secret_key() == key { return Ok(secret_node_weight.id().into()); } } Err(SecretError::NotFoundForKey) } /// Lists all [`Secrets`](Secret) in the current [`snapshot`](crate::WorkspaceSnapshot). #[instrument(name = "list_secrets", level = "debug", skip_all, fields(secret_count = Empty))] pub async fn list(ctx: &DalContext) -> SecretResult<Vec<Self>> { let workspace_snapshot = ctx.workspace_snapshot()?; let mut secrets = vec![]; let secret_category_node_id = workspace_snapshot .get_category_node_or_err(CategoryNodeKind::Secret) .await?; let secret_node_ids = workspace_snapshot .outgoing_targets_for_edge_weight_kind( secret_category_node_id, EdgeWeightKindDiscriminants::Use, ) .await?; let span = Span::current(); span.record("secret_count", secret_node_ids.len()); let mut secret_node_weights = vec![]; let mut hashes = vec![]; for id in secret_node_ids { let secret_node_weight = workspace_snapshot .get_node_weight(id) .await? .get_secret_node_weight()?; hashes.push(secret_node_weight.content_hash()); secret_node_weights.push(secret_node_weight); } let contents: HashMap<ContentHash, SecretContent> = ctx .layer_db() .cas() .try_read_many_as(hashes.as_slice()) .await?; for secret_node_weight in secret_node_weights { match contents.get(&secret_node_weight.content_hash()) { Some(content) => { // NOTE(nick): if we had a v2, then there would be migration logic here. let SecretContent::V1(inner) = content; secrets.push(Self::assemble(secret_node_weight, inner.to_owned())); } None => Err(WorkspaceSnapshotError::MissingContentFromStore( secret_node_weight.id(), ))?, } } Ok(secrets) } /// Updates the metadata for the [`Secret`], but not the encrypted contents. pub async fn update_metadata( self, ctx: &DalContext, name: impl Into<String>, description: Option<String>, ) -> SecretResult<Self> { self.modify(ctx, |s| { s.name = name.into(); s.description = description; Ok(()) }) .await } /// Updates the underlying encrypted contents by generating a new key and inserting a new /// [`EncryptedSecret`]. pub async fn update_encrypted_contents( self, ctx: &DalContext, crypted: &[u8], key_pair_pk: KeyPairPk, version: SecretVersion, algorithm: SecretAlgorithm, ) -> SecretResult<Self> { // Generate a new key and insert a new encrypted secret. let new_key = Self::generate_key(ctx, self.id).await?; // NOTE(nick): we do not clean up the existing encrypted secret yet. EncryptedSecret::insert(ctx, new_key, crypted, key_pair_pk, version, algorithm).await?; // Since we are updating encrypted contents, we have a new key and need to enqueue ourselves // into dependent values update. ctx.add_dependent_values_and_enqueue(vec![self.id]).await?; self.modify(ctx, |s| { s.encrypted_secret_key = new_key; Ok(()) }) .await } /// Finds all secret prop ids for all schema variants #[instrument(name = "find_secret_prop_ids", level = "debug", skip_all)] pub async fn list_all_secret_prop_ids(ctx: &DalContext) -> SecretResult<Vec<PropId>> { let mut result = vec![]; for variant_id in SchemaVariant::list_all_ids(ctx).await? { let secrets_prop = Prop::find_prop_by_path(ctx, variant_id, &RootPropChild::Secrets.prop_path()) .await?; result.extend(Prop::direct_child_prop_ids_ordered(ctx, secrets_prop.id).await?); } Ok(result) } /// Finds all secret prop ids for all schema variants #[instrument(name = "find_secret_prop_ids", level = "debug", skip_all)] pub async fn list_all_secret_prop_ids_for_variant( ctx: &DalContext, schema_variant_id: SchemaVariantId, ) -> SecretResult<Vec<PropId>> { let secrets_prop = Prop::find_prop_by_path(ctx, schema_variant_id, &RootPropChild::Secrets.prop_path()) .await?; Ok(Prop::direct_child_prop_ids_ordered(ctx, secrets_prop.id).await?) } /// Finds all the connected component Ids for the [`Secret`] #[instrument(name = "find_connected_components", level = "debug", skip_all)] pub async fn find_connected_components( self, ctx: &DalContext, prefetched_secret_props: Option<&[PropId]>, ) -> SecretResult<Vec<ComponentId>> { let secret_details = self.encrypted_secret_key().to_string(); let mut connected_components = Vec::new(); // This is a trick to make the borrow checker allow us to return a // reference to the list constructed in the list_all_secret_prop_ids // call, so that we don't have to clone the variants passed in as // `prefetched_secret_props`. let secret_props; for &secret_prop_id in match prefetched_secret_props { Some(variants) => variants, None => { secret_props = Self::list_all_secret_prop_ids(ctx).await?; secret_props.as_slice() } } { let all_connected_attribute_values = Prop::all_attribute_values_everywhere_for_prop_id(ctx, secret_prop_id).await?; for connected_av in all_connected_attribute_values { let av = AttributeValue::get_by_id(ctx, connected_av).await?; if let Some(val) = av.value(ctx).await? { if val == secret_details { let connected_component = AttributeValue::component_id(ctx, connected_av).await?; connected_components.push(connected_component) } } } } Ok(connected_components) } /// Deletes the secret node. pub async fn delete(self, ctx: &DalContext) -> SecretResult<()> { //TODO: Doesn't delete from layer_db or memory at this time! ctx.workspace_snapshot()?.remove_node_by_id(self.id).await?; Ok(()) } async fn modify<L>(self, ctx: &DalContext, lambda: L) -> SecretResult<Self> where L: FnOnce(&mut Self) -> SecretResult<()>, { let mut secret = self; // NOTE(nick): I don't love the current timestamp and actor system. These likely shouldn't // be in the contents, but abstracted out into another service. Because of this, we have to // manually ensure that the actor and timestamp information is correct, regardless of what // the user passes in as the lambda. let before = SecretContentV1::from(secret.clone()); lambda(&mut secret)?; if before != SecretContentV1::from(secret.clone()) { match ctx.history_actor() { HistoryActor::SystemInit => {} HistoryActor::User(id) => { secret.updated_by = Some(*id); } } secret.timestamp.updated_at = Utc::now(); } let (mut secret_node_weight, _) = Self::get_node_weight_and_content_hash_or_error(ctx, secret.id).await?; let workspace_snapshot = ctx.workspace_snapshot()?; // If the encrypted secret key has changed, we end up updating the secret node twice because // we always update the actor and timestamp data if anything has changed. This could be // optimized to do it only once. if secret.encrypted_secret_key() != secret_node_weight.encrypted_secret_key() { secret_node_weight.set_encrypted_secret_key(secret.encrypted_secret_key); workspace_snapshot .add_or_replace_node(NodeWeight::Secret(secret_node_weight.clone())) .await?; } let updated = SecretContentV1::from(secret.clone()); if updated != before { let (hash, _) = ctx.layer_db().cas().write( Arc::new(SecretContent::V1(updated.clone()).into()), None, ctx.events_tenancy(), ctx.events_actor(), )?; ctx.workspace_snapshot()? .update_content(secret.id.into(), hash) .await?; } Ok(Self::assemble(secret_node_weight, updated)) } /// Get the value, formatted for debugging/display. pub async fn fmt_title(ctx: &DalContext, secret_id: SecretId) -> String { Self::fmt_title_fallible(ctx, secret_id) .await .unwrap_or_else(|e| e.to_string()) } async fn fmt_title_fallible(ctx: &DalContext, secret_id: SecretId) -> SecretResult<String> { Ok(format!( "{} ({})", Self::get_by_id(ctx, secret_id).await?.name, secret_id )) } } /// The [`EncryptedSecret`] corresponding to an individual [`Secret`]. It contains sensitive, but /// encrypted, information. #[derive(Clone, Deserialize, Serialize)] pub struct EncryptedSecret { user: Option<UserPk>, version: SecretVersion, algorithm: SecretAlgorithm, key_hash: Hash, key_pair_pk: KeyPairPk, // TODO(nick): confirm that base64 de/ser works as intended. #[serde(with = "nonce_serde")] nonce: SymmetricNonce, // TODO(nick): confirm that base64 de/ser works as intended. #[serde(with = "base64_bytes_serde")] crypted: Vec<u8>, } impl fmt::Debug for EncryptedSecret { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("EncryptedSecret") .field("user", &self.user) .field("version", &self.version) .field("algorithm", &self.algorithm) .field("key_hash", &self.key_hash) .finish_non_exhaustive() } } impl EncryptedSecret { /// This _private_ method is used by [`Secret`] during its creation or when the user wishes /// to "change" its corresponding encrypted contents. async fn insert( ctx: &DalContext, key: EncryptedSecretKey, crypted: &[u8], key_pair_pk: KeyPairPk, version: SecretVersion, algorithm: SecretAlgorithm, ) -> SecretResult<()> { let user = match ctx.history_actor() { HistoryActor::SystemInit => None, HistoryActor::User(user_pk) => Some(*user_pk), }; let (double_crypted, nonce, key_hash) = ctx.symmetric_crypto_service().encrypt(crypted); let value = Self { user, key_pair_pk, nonce, key_hash: key_hash.to_owned(), crypted: double_crypted, version, algorithm, }; ctx.layer_db().encrypted_secret().write( key, Arc::new(value), None, ctx.events_tenancy(), ctx.events_actor(), )?; Ok(()) } /// Gets the [`EncryptedSecret`] with a given key. /// /// _Warning:_ sensitive, but encrypted, contents will be returned. pub async fn get_by_key( ctx: &DalContext, key: EncryptedSecretKey, ) -> SecretResult<Option<Self>> { Ok(ctx.layer_db().encrypted_secret().try_read_as(&key).await?) } /// Decrypts the encrypted secret with its associated [`KeyPair`] and returns a /// [`DecryptedSecret`]. pub async fn decrypt(self, ctx: &DalContext) -> SecretResult<DecryptedSecret> { let key_pair = self.key_pair(ctx).await?; self.into_decrypted( key_pair.public_key(), key_pair.secret_key(), ctx.symmetric_crypto_service(), ) } fn into_decrypted( self, pkey: &PublicKey, skey: &SecretKey, symmetric_crypto_service: &SymmetricCryptoService, ) -> SecretResult<DecryptedSecret> { // Explicitly match on (version, algorithm) tuple to ensure that any new // versions/algorithms will trigger a compilation failure match (self.version, self.algorithm) { (SecretVersion::V1, SecretAlgorithm::Sealedbox) => { let symmetric_decrypted = symmetric_crypto_service.decrypt(&self.crypted, &self.nonce, &self.key_hash)?; let message = serde_json::from_slice( &sealedbox::open(&symmetric_decrypted, pkey, skey) .map_err(|_| SecretError::DecryptionFailed)?, ) .map_err(SecretError::DeserializeMessage)?; Ok(DecryptedSecret { message }) } } } /// Gets the [`KeyPair`] corresponding to the [`KeyPairPk`] on the [`EncryptedSecret`]. pub async fn key_pair(&self, ctx: &DalContext) -> SecretResult<KeyPair> { Ok(KeyPair::get_by_pk(ctx, self.key_pair_pk).await?) } } /// This type corresponds to a secret that has been decrypted. It is returned by calling /// [`EncryptedSecret::decrypt`], which contains the raw decrypted message, and without the /// encrypted payload and other metadata. It is not persist-able and is only intended to be used /// internally when passing secrets through to "cyclone". /// /// _Note:_ we're being a bit careful here as to which traits are derived in an effort to minimize /// leaking sensitive data. #[derive(Serialize)] pub struct DecryptedSecret { message: Value, } impl DecryptedSecret { pub(crate) fn message(&self) -> SensitiveContainer<Value> { self.message.clone().into() } } impl fmt::Debug for DecryptedSecret { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("DecryptedSecret").finish_non_exhaustive() } } impl From<AttributeValueError> for SecretError { fn from(value: AttributeValueError) -> Self { Box::new(value).into() } } impl From<ComponentError> for SecretError { fn from(value: ComponentError) -> Self { Box::new(value).into() } } impl From<SchemaVariantError> for SecretError { fn from(value: SchemaVariantError) -> Self { Box::new(value).into() } } impl From<AttributePrototypeError> for SecretError { fn from(value: AttributePrototypeError) -> Self { Box::new(value).into() } } impl From<AttributePrototypeArgumentError> for SecretError { fn from(value: AttributePrototypeArgumentError) -> Self { Box::new(value).into() } } impl From<FuncError> for SecretError { fn from(value: FuncError) -> Self { Box::new(value).into() } } impl From<FuncArgumentError> for SecretError { fn from(value: FuncArgumentError) -> Self { Box::new(value).into() } } impl From<PropError> for SecretError { fn from(value: PropError) -> Self { Box::new(value).into() } } #[cfg(test)] mod tests { use sodiumoxide::crypto::box_; use super::*; fn encrypted_secret( crypted: impl Into<Vec<u8>>, symmetric_crypto_service: &SymmetricCryptoService, ) -> EncryptedSecret { let crypted = crypted.into(); let (double_crypted, nonce, key_hash) = symmetric_crypto_service.encrypt(crypted.as_ref()); EncryptedSecret { user: None, key_pair_pk: KeyPairPk::NONE, nonce, key_hash: *key_hash, crypted: double_crypted, version: Default::default(), algorithm: Default::default(), } } fn crypt<T>(value: &T, pkey: &PublicKey) -> Vec<u8> where T: ?Sized + Serialize, { sealedbox::seal( &serde_json::to_vec(value).expect("failed to serialize value"), pkey, ) } #[test] fn into_decrypted() { sodiumoxide::init().expect("crypto failed to init"); let (pkey, skey) = box_::gen_keypair(); let message = serde_json::json!({"username": "The Cadillac Three", "password": "Slow Rollin"}); let crypted = crypt(&message, &pkey); let service = SymmetricCryptoService::new(SymmetricCryptoService::generate_key(), vec![]); let encrypted = encrypted_secret(crypted, &service); let decrypted = encrypted .into_decrypted(&pkey, &skey, &service) .expect("could not decrypt secret"); assert_eq!(message, decrypted.message); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server