An encrypted personal cloud built on the AT Protocol.

Merge branch 'feature/require-directory-on-upload'

sans-self.org 7b92b71f e059ac89

Waiting for spindle ...
+55 -46
+3 -1
CHANGELOG.md
··· 9 9 ### Security 10 10 - Fix ContentKey Debug impl to redact secret bytes [#49](https://issues.opake.app/issues/49.html) 11 11 - Add file permission hardening for sensitive config and key files [#8](https://issues.opake.app/issues/8.html) 12 - - Remove bearer token authentication fallback from AppView [#26](https://issues.opake.app/issues/26.html) 12 + - Remove bearer token authentication fallback from AppView [#26](https://issues.opake.app/issues/26.html)s 13 13 14 14 ### Added 15 15 - Add metadata CLI command for rename, tag, and description management [#190](https://issues.opake.app/issues/190.html) ··· 62 62 - Update login command to read password from stdin [#112](https://issues.opake.app/issues/112.html) 63 63 64 64 ### Fixed 65 + - Require directory on upload, default to root when --dir is omitted [#192](https://issues.opake.app/issues/192.html) 65 66 - Fix bugs found during black-box integration testing of sharing workflow [#70](https://issues.opake.app/issues/70.html) 66 67 - Fix base64 padding mismatch when decoding PDS $bytes fields [#68](https://issues.opake.app/issues/68.html) 67 68 - Fix missing HTTP status checks in XRPC client [#104](https://issues.opake.app/issues/104.html) 68 69 69 70 ### Changed 71 + - Add metadata CLI command with tag and description subcommands [#190](https://issues.opake.app/issues/190.html) 70 72 - Encrypt directory metadata (add encryption envelope to directory records) [#189](https://issues.opake.app/issues/189.html) 71 73 - Encrypt keyring and grant metadata [#188](https://issues.opake.app/issues/188.html) 72 74 - Encrypt document metadata (name, mimeType, tags, description) [#187](https://issues.opake.app/issues/187.html)
+2 -35
crates/opake-cli/src/commands/mkdir.rs
··· 2 2 use chrono::Utc; 3 3 use clap::Args; 4 4 use opake_core::client::Session; 5 - use opake_core::crypto::{self, DirectoryMetadata, OsRng}; 5 + use opake_core::crypto::OsRng; 6 6 use opake_core::directories; 7 - use opake_core::records::{DirectEncryption, Encryption, EncryptionEnvelope}; 8 7 9 - use crate::commands::Execute; 8 + use crate::commands::{encrypt_directory, Execute}; 10 9 use crate::identity; 11 10 use crate::session::{self, CommandContext}; 12 11 ··· 15 14 pub struct MkdirCommand { 16 15 /// Name for the directory 17 16 name: String, 18 - } 19 - 20 - /// Build a direct encryption envelope for a directory. 21 - /// 22 - /// Generates a fresh content key, encrypts the metadata with it, and wraps 23 - /// the key to the owner's public key. Returns the encryption enum and 24 - /// encrypted metadata for the directory record. 25 - fn encrypt_directory( 26 - name: &str, 27 - owner_did: &str, 28 - owner_pubkey: &crypto::X25519PublicKey, 29 - rng: &mut (impl crypto::CryptoRng + crypto::RngCore), 30 - ) -> Result<(Encryption, opake_core::records::EncryptedMetadata)> { 31 - let content_key = crypto::generate_content_key(rng); 32 - 33 - let metadata = DirectoryMetadata { 34 - name: name.into(), 35 - description: None, 36 - }; 37 - let encrypted_metadata = crypto::encrypt_metadata(&content_key, &metadata, rng)?; 38 - 39 - let wrapped_key = crypto::wrap_key(&content_key, owner_pubkey, owner_did, rng)?; 40 - 41 - let encryption = Encryption::Direct(DirectEncryption { 42 - envelope: EncryptionEnvelope { 43 - algo: "aes-256-gcm".into(), 44 - nonce: opake_core::records::AtBytes::from_raw(&[0u8; 12]), 45 - keys: vec![wrapped_key], 46 - }, 47 - }); 48 - 49 - Ok((encryption, encrypted_metadata)) 50 17 } 51 18 52 19 impl Execute for MkdirCommand {
+35
crates/opake-cli/src/commands/mod.rs
··· 21 21 22 22 use anyhow::Result; 23 23 use opake_core::client::Session; 24 + use opake_core::crypto::{self, DirectoryMetadata}; 25 + use opake_core::records::{ 26 + AtBytes, DirectEncryption, EncryptedMetadata, Encryption, EncryptionEnvelope, 27 + }; 24 28 25 29 use crate::session::CommandContext; 26 30 ··· 30 34 ctx: &CommandContext, 31 35 ) -> impl std::future::Future<Output = Result<Option<Session>>>; 32 36 } 37 + 38 + /// Build a direct encryption envelope for a directory. 39 + /// 40 + /// Generates a fresh content key, encrypts the metadata with it, and wraps 41 + /// the key to the owner's public key. 42 + pub fn encrypt_directory( 43 + name: &str, 44 + owner_did: &str, 45 + owner_pubkey: &crypto::X25519PublicKey, 46 + rng: &mut (impl crypto::CryptoRng + crypto::RngCore), 47 + ) -> Result<(Encryption, EncryptedMetadata)> { 48 + let content_key = crypto::generate_content_key(rng); 49 + 50 + let metadata = DirectoryMetadata { 51 + name: name.into(), 52 + description: None, 53 + }; 54 + let encrypted_metadata = crypto::encrypt_metadata(&content_key, &metadata, rng)?; 55 + 56 + let wrapped_key = crypto::wrap_key(&content_key, owner_pubkey, owner_did, rng)?; 57 + 58 + let encryption = Encryption::Direct(DirectEncryption { 59 + envelope: EncryptionEnvelope { 60 + algo: "aes-256-gcm".into(), 61 + nonce: AtBytes::from_raw(&[0u8; 12]), 62 + keys: vec![wrapped_key], 63 + }, 64 + }); 65 + 66 + Ok((encryption, encrypted_metadata)) 67 + }
+15 -10
crates/opake-cli/src/commands/upload.rs
··· 12 12 13 13 use opake_core::client::Session; 14 14 15 - use crate::commands::Execute; 15 + use crate::commands::{encrypt_directory, Execute}; 16 16 use crate::identity; 17 17 use crate::keyring_store; 18 18 use crate::session::{self, CommandContext}; ··· 31 31 #[arg(long)] 32 32 description: Option<String>, 33 33 34 - /// Place the uploaded document into a directory 34 + /// Directory to place the document in (defaults to root "/") 35 35 #[arg(long)] 36 36 dir: Option<String>, 37 37 } ··· 39 39 impl Execute for UploadCommand { 40 40 async fn execute(self, ctx: &CommandContext) -> Result<Option<Session>> { 41 41 let mut client = session::load_client(&ctx.storage, &ctx.did)?; 42 + let id = identity::load_identity(&ctx.storage, &ctx.did)?; 42 43 43 44 let plaintext = 44 45 fs::read(&self.path).context(format!("failed to read {}", self.path.display()))?; ··· 56 57 let now = Utc::now().to_rfc3339(); 57 58 58 59 let uri = if let Some(keyring_name) = &self.keyring { 59 - let id = identity::load_identity(&ctx.storage, &ctx.did)?; 60 60 let private_key = id.private_key_bytes()?; 61 61 let entry = 62 62 keyrings::resolve_keyring_uri(&mut client, keyring_name, &id.did, &private_key) ··· 82 82 83 83 documents::encrypt_and_upload_keyring(&mut client, &params, &mut OsRng).await? 84 84 } else { 85 - let id = identity::load_identity(&ctx.storage, &ctx.did)?; 86 85 let owner_pubkey = id.public_key_bytes()?; 87 86 88 87 let params = UploadParams { ··· 98 97 documents::encrypt_and_upload(&mut client, &params, &mut OsRng).await? 99 98 }; 100 99 101 - if let Some(dir_path) = &self.dir { 102 - let id = identity::load_identity(&ctx.storage, &ctx.did)?; 100 + let (directory_uri, dir_label) = if let Some(dir_path) = &self.dir { 103 101 let private_key = id.private_key_bytes()?; 104 102 let mut tree = DirectoryTree::load(&mut client).await?; 105 103 tree.decrypt_names(&ctx.did, &private_key); ··· 109 107 anyhow::bail!("{dir_path:?} is not a directory"); 110 108 } 111 109 112 - directories::add_entry(&mut client, &resolved.uri, &uri, &now).await?; 113 - println!("{} → {} (in {})", filename, uri, dir_path); 110 + (resolved.uri, dir_path.clone()) 114 111 } else { 115 - println!("{} → {}", filename, uri); 116 - } 112 + let pubkey = id.public_key_bytes()?; 113 + let (root_enc, root_meta) = encrypt_directory("/", &ctx.did, &pubkey, &mut OsRng)?; 114 + let root_uri = 115 + directories::get_or_create_root(&mut client, &ctx.did, root_enc, root_meta, &now) 116 + .await?; 117 + (root_uri, "/".into()) 118 + }; 119 + 120 + directories::add_entry(&mut client, &directory_uri, &uri, &now).await?; 121 + println!("{} → {} (in {})", filename, uri, dir_label); 117 122 118 123 Ok(session::refreshed_session(&client)) 119 124 }