diff --git a/.github/workflows/noosphere_apple_build.yaml b/.github/workflows/noosphere_apple_build.yaml index cf105dc16..3409a56df 100644 --- a/.github/workflows/noosphere_apple_build.yaml +++ b/.github/workflows/noosphere_apple_build.yaml @@ -65,7 +65,7 @@ jobs: brew: protobuf cmake - name: 'Generate the header' run: | - cd rust/noosphere/include + cd rust/noosphere/include/noosphere cargo run --example generate_header --features headers --locked cd - - uses: actions/upload-artifact@v3 diff --git a/Cargo.lock b/Cargo.lock index b71379a85..b87418e29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2263,15 +2263,28 @@ name = "noosphere" version = "0.1.0-alpha.1" dependencies = [ "anyhow", + "async-trait", + "cid", + "js-sys", "lazy_static", "noosphere-api", "noosphere-core", "noosphere-fs", "noosphere-storage", + "pollster", + "rexie", "safer-ffi", + "temp-dir", "thiserror", + "tokio", + "tracing", + "tracing-wasm", "ucan", "ucan-key-support", + "url", + "wasm-bindgen", + "wasm-bindgen-test", + "web-sys", ] [[package]] @@ -2836,6 +2849,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + [[package]] name = "poly1305" version = "0.7.2" @@ -4153,6 +4172,17 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + [[package]] name = "trust-dns-proto" version = "0.22.0" diff --git a/Package.swift b/Package.swift new file mode 100644 index 000000000..ec47afa8b --- /dev/null +++ b/Package.swift @@ -0,0 +1,37 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. +import PackageDescription + +let package = Package( + name: "SwiftNoosphere", + platforms: [ + .iOS(.v13), + .macOS(.v11) + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "SwiftNoosphere", + targets: ["SwiftNoosphere"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "SwiftNoosphere", + dependencies: ["LibNoosphere"], + path: "swift/Sources/SwiftNoosphere"), + .binaryTarget( + name: "LibNoosphere", + url: "https://github.com/subconsciousnetwork/swift-noosphere/releases/download/v0.1.2-alpha.1/libnoosphere.zip", + checksum: "462dc8c1c4207efbf71ead4e0dfe70a7d2153ebd4189b7eb5d9c0c9c3dd68d6e"), + .testTarget( + name: "SwiftNoosphereTests", + dependencies: ["SwiftNoosphere"], + path: "swift/Tests/SwiftNoosphereTests"), + ] +) \ No newline at end of file diff --git a/rust/noosphere-api/src/client.rs b/rust/noosphere-api/src/client.rs index 2ba30bd1c..c92e50f45 100644 --- a/rust/noosphere-api/src/client.rs +++ b/rust/noosphere-api/src/client.rs @@ -21,33 +21,33 @@ use ucan::{ }; use url::Url; -pub struct Client<'a, K, S> +pub struct Client where - K: KeyMaterial, + K: KeyMaterial + 'static, S: UcanStore, { pub session: IdentifyResponse, pub sphere_identity: String, pub api_base: Url, - pub credential: &'a K, + pub credential: K, pub authorization: Authorization, pub store: S, client: reqwest::Client, } -impl<'a, K, S> Client<'a, K, S> +impl Client where - K: KeyMaterial, + K: KeyMaterial + 'static, S: UcanStore, { pub async fn identify( sphere_identity: &str, api_base: &Url, - credential: &'a K, + credential: K, authorization: &Authorization, did_parser: &mut DidParser, store: S, - ) -> Result> { + ) -> Result> { debug!("Initializing Noosphere API client"); debug!("Client represents sphere {}", sphere_identity); debug!("Client targetting API at {}", api_base); @@ -64,7 +64,7 @@ where let (jwt, ucan_headers) = Self::make_bearer_token( &gateway_identity, - credential, + &credential, authorization, &Capability { with: With::Resource { @@ -107,7 +107,7 @@ where async fn make_bearer_token( gateway_identity: &str, - credential: &'a K, + credential: &K, authorization: &Authorization, capability: &Capability, store: &S, @@ -180,7 +180,7 @@ where let (token, ucan_headers) = Self::make_bearer_token( &self.session.gateway_identity, - self.credential, + &self.credential, &self.authorization, &capability, &self.store, @@ -219,7 +219,7 @@ where let (token, ucan_headers) = Self::make_bearer_token( &self.session.gateway_identity, - self.credential, + &self.credential, &self.authorization, &capability, &self.store, diff --git a/rust/noosphere-cli/src/native/commands/serve/gateway.rs b/rust/noosphere-cli/src/native/commands/serve/gateway.rs index 728c3b736..11721b643 100644 --- a/rust/noosphere-cli/src/native/commands/serve/gateway.rs +++ b/rust/noosphere-cli/src/native/commands/serve/gateway.rs @@ -190,7 +190,7 @@ mod tests { let client = Client::identify( &client_sphere_identity, &api_base, - &client_key, + client_key.clone(), &client_authorization, &mut did_parser, client_db, @@ -276,7 +276,7 @@ mod tests { let client = Client::identify( &client_sphere_identity, &api_base, - &client_key, + client_key.clone(), &client_authorization, &mut did_parser, client_db.clone(), @@ -395,7 +395,7 @@ mod tests { let client = Client::identify( &client_sphere_identity, &api_base, - &client_key, + client_key.clone(), &client_authorization, &mut did_parser, client_db.clone(), @@ -556,7 +556,7 @@ mod tests { let client = Client::identify( &client_sphere_identity, &api_base, - &client_key, + client_key.clone(), &client_authorization, &mut did_parser, client_db.clone(), diff --git a/rust/noosphere-cli/src/native/commands/sync.rs b/rust/noosphere-cli/src/native/commands/sync.rs index c254e6d44..ad9f76b1a 100644 --- a/rust/noosphere-cli/src/native/commands/sync.rs +++ b/rust/noosphere-cli/src/native/commands/sync.rs @@ -46,7 +46,7 @@ pub async fn sync(workspace: &Workspace) -> Result<()> { let client = Client::identify( &sphere_identity, &gateway_url, - &key, + key.clone(), &authorization, &mut did_parser, db.clone(), @@ -75,9 +75,9 @@ pub async fn sync(workspace: &Workspace) -> Result<()> { /// Attempts to push the latest local lineage to the gateway, causing the /// gateway to update its own pointer to the tip of the local sphere's history -pub async fn push_local_changes<'a, S, K>( +pub async fn push_local_changes( local_sphere_identity: &str, - client: &Client<'a, K, SphereDb>, + client: &Client>, db: &mut SphereDb, ) -> Result<()> where @@ -172,10 +172,10 @@ where /// Fetches the latest changes from a gateway and updates the local lineage /// using a conflict-free rebase strategy -pub async fn sync_remote_changes<'a, K, S>( +pub async fn sync_remote_changes( local_sphere_identity: &str, - client: &Client<'a, K, SphereDb>, - credential: &'a K, + client: &Client>, + credential: &K, authorization: Option<&Authorization>, db: &mut SphereDb, ) -> Result<()> diff --git a/rust/noosphere-cli/src/native/workspace.rs b/rust/noosphere-cli/src/native/workspace.rs index b24e37d5f..f6e7718c1 100644 --- a/rust/noosphere-cli/src/native/workspace.rs +++ b/rust/noosphere-cli/src/native/workspace.rs @@ -589,7 +589,7 @@ The available keys are: /// Creates all the directories needed to start rendering a sphere in the /// configured working file tree root pub async fn initialize_local_directories(&self) -> Result<()> { - if let Ok(_) = self.expect_local_directories() { + if self.expect_local_directories().is_ok() { return Err(anyhow!( r#"Cannot initialize the sphere; a sphere is already initialized in {:?} Unexpected (bad) things will happen if you try to nest spheres this way!"#, diff --git a/rust/noosphere-core/src/data/headers/version.rs b/rust/noosphere-core/src/data/headers/version.rs index a2938bd7b..03d5c5c3e 100644 --- a/rust/noosphere-core/src/data/headers/version.rs +++ b/rust/noosphere-core/src/data/headers/version.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use std::{convert::Infallible, fmt::Display, str::FromStr}; pub enum Version { @@ -26,3 +27,14 @@ impl FromStr for Version { }) } } + +impl TryFrom for u32 { + type Error = anyhow::Error; + + fn try_from(value: Version) -> Result { + match value { + Version::V0 => Ok(0), + Version::Unknown(version) => Err(anyhow!("Unrecognized version: {}", version)), + } + } +} diff --git a/rust/noosphere-core/src/view/sphere.rs b/rust/noosphere-core/src/view/sphere.rs index 0f116c925..974061a60 100644 --- a/rust/noosphere-core/src/view/sphere.rs +++ b/rust/noosphere-core/src/view/sphere.rs @@ -17,7 +17,7 @@ use crate::{ }, data::{ AuthorityIpld, Bundle, CidKey, ContentType, DelegationIpld, Header, MemoIpld, - RevocationIpld, SphereIpld, TryBundle, + RevocationIpld, SphereIpld, TryBundle, Version, }, view::{Links, SphereMutation, SphereRevision, Timeline}, }; @@ -352,6 +352,9 @@ impl Sphere { ContentType::Sphere.to_string(), )); + memo.headers + .push((Header::Version.to_string(), Version::V0.to_string())); + let capability = Capability { with: With::Resource { kind: Resource::Scoped(SphereReference { diff --git a/rust/noosphere-storage/src/db.rs b/rust/noosphere-storage/src/db.rs index f25cf21d1..2090b625e 100644 --- a/rust/noosphere-storage/src/db.rs +++ b/rust/noosphere-storage/src/db.rs @@ -1,16 +1,21 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use cid::Cid; +use libipld_cbor::DagCborCodec; use libipld_core::{ codec::{Codec, Decode, Encode, References}, ipld::Ipld, raw::RawCodec, }; +use serde::{de::DeserializeOwned, Serialize}; use std::{collections::BTreeSet, fmt::Debug}; -use tokio_stream::{Stream}; +use tokio_stream::Stream; use ucan::store::{UcanStore, UcanStoreConditionalSend}; -use crate::interface::{BlockStore, KeyValueStore, StorageProvider, Store}; +use crate::{ + interface::{BlockStore, BlockStoreSend, KeyValueStore, StorageProvider, Store}, + memory::MemoryStore, +}; use async_stream::try_stream; @@ -29,6 +34,10 @@ impl SphereDbSendSync for T {} pub const BLOCK_STORE: &str = "blocks"; pub const LINK_STORE: &str = "links"; pub const VERSION_STORE: &str = "versions"; +pub const METADATA_STORE: &str = "metadata"; + +pub const SPHERE_DB_STORE_NAMES: &[&str] = + &[BLOCK_STORE, LINK_STORE, VERSION_STORE, METADATA_STORE]; #[derive(Clone)] pub struct SphereDb @@ -38,6 +47,7 @@ where block_store: S, link_store: S, version_store: S, + metadata_store: S, } impl SphereDb @@ -49,9 +59,31 @@ where block_store: storage_provider.get_store(BLOCK_STORE).await?, link_store: storage_provider.get_store(LINK_STORE).await?, version_store: storage_provider.get_store(VERSION_STORE).await?, + metadata_store: storage_provider.get_store(METADATA_STORE).await?, }) } + pub async fn persist(&mut self, memory_store: &MemoryStore) -> Result<()> { + let cids = memory_store.get_stored_cids().await; + + for cid in &cids { + let block = memory_store.require_block(cid).await?; + + self.put_block(cid, &block).await?; + + match cid.codec() { + codec_id if codec_id == u64::from(DagCborCodec) => { + self.put_links::(cid, &block).await?; + } + codec_id if codec_id == u64::from(RawCodec) => { + self.put_links::(cid, &block).await?; + } + codec_id => warn!("Unrecognized codec {}; skipping...", codec_id), + } + } + Ok(()) + } + pub async fn set_version(&mut self, identity: &str, version: &Cid) -> Result<()> { self.version_store .set_key(identity.to_string(), version) @@ -143,6 +175,30 @@ where } } +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl KeyValueStore for SphereDb +where + S: Store, +{ + async fn set_key(&mut self, key: K, value: V) -> Result<()> + where + K: AsRef<[u8]> + BlockStoreSend, + V: Serialize + BlockStoreSend, + { + self.metadata_store.set_key(key, value).await?; + Ok(()) + } + + async fn get_key(&self, key: K) -> Result> + where + K: AsRef<[u8]> + BlockStoreSend, + V: DeserializeOwned + BlockStoreSend, + { + self.metadata_store.get_key(key).await + } +} + #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] impl UcanStore for SphereDb diff --git a/rust/noosphere-storage/src/helpers.rs b/rust/noosphere-storage/src/helpers.rs index c02f22134..6a99cbd79 100644 --- a/rust/noosphere-storage/src/helpers.rs +++ b/rust/noosphere-storage/src/helpers.rs @@ -30,6 +30,6 @@ pub async fn make_disposable_store() -> Result { .map(|word| String::from(word)) .collect(); - let provider = WebStorageProvider::new(1, &temp_name, &vec!["foo"]).await?; - provider.get_store("foo").await + let provider = WebStorageProvider::new(&temp_name).await?; + provider.get_store(crate::db::BLOCK_STORE).await } diff --git a/rust/noosphere-storage/src/interface.rs b/rust/noosphere-storage/src/interface.rs index e324c941b..4f4c32404 100644 --- a/rust/noosphere-storage/src/interface.rs +++ b/rust/noosphere-storage/src/interface.rs @@ -1,4 +1,4 @@ -use std::io::Cursor; +use std::{fmt::Display, io::Cursor}; use anyhow::{anyhow, Result}; use async_trait::async_trait; @@ -182,7 +182,37 @@ where #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait KeyValueStore: Store { +pub trait KeyValueStore { + async fn set_key(&mut self, key: K, value: V) -> Result<()> + where + K: AsRef<[u8]> + BlockStoreSend, + V: Serialize + BlockStoreSend; + + async fn get_key(&self, key: K) -> Result> + where + K: AsRef<[u8]> + BlockStoreSend, + V: DeserializeOwned + BlockStoreSend; + + async fn require_key(&self, key: K) -> Result + where + K: AsRef<[u8]> + BlockStoreSend + Display, + V: DeserializeOwned + BlockStoreSend, + { + let required = key.to_string(); + + match self.get_key(key).await? { + Some(value) => Ok(value), + None => Err(anyhow!("No value found for '{required}'")), + } + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl KeyValueStore for S +where + S: Store, +{ async fn set_key(&mut self, key: K, value: V) -> Result<()> where K: AsRef<[u8]> + BlockStoreSend, @@ -211,5 +241,3 @@ pub trait KeyValueStore: Store { }) } } - -impl KeyValueStore for S where S: Store {} diff --git a/rust/noosphere-storage/src/native.rs b/rust/noosphere-storage/src/native.rs index 933d075b0..535b5e356 100644 --- a/rust/noosphere-storage/src/native.rs +++ b/rust/noosphere-storage/src/native.rs @@ -18,7 +18,10 @@ pub struct NativeStorageProvider { impl NativeStorageProvider { pub fn new(init: NativeStorageInit) -> Result { let db: Db = match init { - NativeStorageInit::Path(path) => sled::open(path)?, + NativeStorageInit::Path(path) => { + std::fs::create_dir_all(&path)?; + sled::open(path)? + } NativeStorageInit::Db(db) => db, }; diff --git a/rust/noosphere-storage/src/web.rs b/rust/noosphere-storage/src/web.rs index 00d08e487..82eec5feb 100644 --- a/rust/noosphere-storage/src/web.rs +++ b/rust/noosphere-storage/src/web.rs @@ -7,13 +7,20 @@ use rexie::{ }; use std::rc::Rc; use wasm_bindgen::{JsCast, JsValue}; +use crate::db::SPHERE_DB_STORE_NAMES; + +pub const INDEXEDDB_STORAGE_VERSION: u32 = 1; pub struct WebStorageProvider { db: Rc, } impl WebStorageProvider { - pub async fn new(version: u32, db_name: &str, store_names: &[&str]) -> Result { + pub async fn new(db_name: &str) -> Result { + Self::configure(INDEXEDDB_STORAGE_VERSION, db_name, SPHERE_DB_STORE_NAMES).await + } + + async fn configure(version: u32, db_name: &str, store_names: &[&str]) -> Result { let mut builder = RexieBuilder::new(db_name).version(version); for name in store_names { diff --git a/rust/noosphere/Cargo.toml b/rust/noosphere/Cargo.toml index 1090a4330..4ad4b6c92 100644 --- a/rust/noosphere/Cargo.toml +++ b/rust/noosphere/Cargo.toml @@ -26,7 +26,10 @@ headers = ["safer-ffi/headers"] anyhow = "^1" thiserror = "^1" lazy_static = "^1" - +cid = "~0.8" +async-trait = "~0.1" +tracing = "~0.1" +url = "^2" noosphere-core = { version = "0.1.0-alpha.1", path = "../noosphere-core" } noosphere-fs = { version = "0.1.0-alpha.1", path = "../noosphere-fs" } @@ -35,5 +38,26 @@ noosphere-api = { version = "0.1.0", path = "../noosphere-api" } ucan = { version = "0.7.0-alpha.1" } ucan-key-support = { version = "0.7.0-alpha.1" } +[dev-dependencies] +wasm-bindgen-test = "~0.3" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +tokio = { version = "^1", features = ["sync"] } +rexie = { version = "~0.4" } +wasm-bindgen = "~0.2" +js-sys = "~0.3" +tracing-wasm = "~0.2" + +[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] +version = "~0.3" +features = [ + "CryptoKey", +] + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -safer-ffi = { version = "~0.0.10", features = ["proc_macros"] } \ No newline at end of file +safer-ffi = { version = "~0.0.10", features = ["proc_macros"] } +pollster = "~0.2" +tokio = { version = "^1", features = ["full"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +temp-dir = "0.1" \ No newline at end of file diff --git a/rust/noosphere/examples/generate_header.rs b/rust/noosphere/examples/generate_header.rs index 93d739293..911fe87c7 100644 --- a/rust/noosphere/examples/generate_header.rs +++ b/rust/noosphere/examples/generate_header.rs @@ -1,3 +1,7 @@ +///! This example is used to generate the FFI interface C header. You can run +///! it locally to generate a noosphere.h that represents the FFI interface +///! exposed by this crate at any given revision. + fn main() -> std::io::Result<()> { #[cfg(feature = "headers")] noosphere::ffi::generate_headers()?; diff --git a/rust/noosphere/include/module.modulemap b/rust/noosphere/include/noosphere/module.modulemap similarity index 100% rename from rust/noosphere/include/module.modulemap rename to rust/noosphere/include/noosphere/module.modulemap diff --git a/rust/noosphere/src/builder.rs b/rust/noosphere/src/builder.rs deleted file mode 100644 index 90d8e77e1..000000000 --- a/rust/noosphere/src/builder.rs +++ /dev/null @@ -1,9 +0,0 @@ -use anyhow::{anyhow, Result}; -use noosphere_api::{ - client::Client, - data::{FetchParameters, FetchResponse}, -}; -use noosphere_core::{authority::Authorization, view::Sphere}; -use noosphere_fs::SphereFs; -use noosphere_storage::{db::SphereDb, interface::Store}; -use ucan::crypto::KeyMaterial; diff --git a/rust/noosphere/src/error.rs b/rust/noosphere/src/error.rs index a436fdef3..1463ae22a 100644 --- a/rust/noosphere/src/error.rs +++ b/rust/noosphere/src/error.rs @@ -5,6 +5,12 @@ pub enum NoosphereError { #[error("Network access required but network is currently offline")] NetworkOffline, + #[error("No credentials configured")] + NoCredentials, + + #[error("Missing configuration: {0}")] + MissingConfiguration(&'static str), + #[error("{0}")] Other(anyhow::Error), } diff --git a/rust/noosphere/src/ffi.rs b/rust/noosphere/src/ffi.rs deleted file mode 100644 index fa3c6f3ec..000000000 --- a/rust/noosphere/src/ffi.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::collections::BTreeMap; - -use lazy_static::lazy_static; -use safer_ffi::prelude::*; - -use crate::{ - platform::{PlatformKeyMaterial, PlatformStore}, - sphere::SphereContext, -}; - -lazy_static! { - static ref ACTIVE_SPHERES: BTreeMap>> = - BTreeMap::new(); -} - -static mut GLOBAL_STORAGE_PATH: Option = None; -static mut SPHERE_STORAGE_PATH: Option = None; - -ReprC! { - #[repr(C)] - pub struct SphereHandle { - user_identity: char_p::Box, - sphere_identity: char_p::Box, - } - -} - -ReprC! { - #[repr(C)] - pub struct SphereCreationResult { - mnemonic: char_p::Box - } -} - -/// Set the global Noosphere storage path. This will be used to store user -/// data and configuration that is cross-cutting with respect to sphere data -#[ffi_export] -pub fn noosphere_set_global_storage_path(path: char_p::Ref<'_>) { - unsafe { - GLOBAL_STORAGE_PATH = Some(path.to_string()); - } -} - -/// Set the sphere storage path. This will be used as the root for storing -/// sphere data. Spheres will be stored in distinctive sub-hierarchies within -/// this path. -#[ffi_export] -pub fn noosphere_set_sphere_storage_path(path: char_p::Ref<'_>) { - unsafe { - SPHERE_STORAGE_PATH = Some(path.to_string()); - } -} - -#[cfg(feature = "headers")] -pub fn generate_headers() -> std::io::Result<()> { - safer_ffi::headers::builder() - .to_file("noosphere.h")? - .generate() -} diff --git a/rust/noosphere/src/ffi/key.rs b/rust/noosphere/src/ffi/key.rs new file mode 100644 index 000000000..0c57a1458 --- /dev/null +++ b/rust/noosphere/src/ffi/key.rs @@ -0,0 +1,10 @@ +use safer_ffi::prelude::*; + +use crate::ffi::NsNoosphereContext; + +#[ffi_export] +/// Create a key with the given name in the current platform's support key +/// storage mechanism. +pub fn ns_key_create(noosphere: &NsNoosphereContext, name: char_p::Ref<'_>) { + pollster::block_on(noosphere.inner().create_key(name.to_str())).unwrap(); +} diff --git a/rust/noosphere/src/ffi/mod.rs b/rust/noosphere/src/ffi/mod.rs new file mode 100644 index 000000000..d4c828d66 --- /dev/null +++ b/rust/noosphere/src/ffi/mod.rs @@ -0,0 +1,16 @@ +///! This module contains FFI implementation for all C ABI-speaking language +///! integrations. +mod key; +mod noosphere; +mod sphere; + +pub use crate::ffi::noosphere::*; +pub use key::*; +pub use sphere::*; + +#[cfg(feature = "headers")] +pub fn generate_headers() -> std::io::Result<()> { + safer_ffi::headers::builder() + .to_file("noosphere.h")? + .generate() +} diff --git a/rust/noosphere/src/ffi/noosphere.rs b/rust/noosphere/src/ffi/noosphere.rs new file mode 100644 index 000000000..9a41c8f7b --- /dev/null +++ b/rust/noosphere/src/ffi/noosphere.rs @@ -0,0 +1,74 @@ +use anyhow::Result; +use safer_ffi::prelude::*; +use url::Url; + +use crate::noosphere::{NoosphereContext, NoosphereContextConfiguration}; + +ReprC! { + #[ReprC::opaque] + pub struct NsNoosphereContext { + inner: NoosphereContext + } +} + +impl NsNoosphereContext { + pub fn new( + global_storage_path: &str, + sphere_storage_path: &str, + gateway_url: Option<&Url>, + ) -> Result { + Ok(NsNoosphereContext { + inner: NoosphereContext::new(NoosphereContextConfiguration::Insecure { + global_storage_path: global_storage_path.into(), + sphere_storage_path: sphere_storage_path.into(), + gateway_url: gateway_url.cloned(), + })?, + }) + } + + pub fn inner(&self) -> &NoosphereContext { + &self.inner + } + + pub fn inner_mut(&mut self) -> &mut NoosphereContext { + &mut self.inner + } +} + +#[ffi_export] +/// Initialize a [NoosphereContext] and return a boxed pointer to it. This is +/// the entrypoint to the Noosphere API, and the returned pointer is used to +/// invoke almost all other API functions. +/// +/// In order to initialize the [NoosphereContext], you must provide two +/// namespace strings: one for "global" Noosphere configuration, and another +/// for sphere storage. Note that at this time "global" configuration is only +/// used for insecure, on-disk key storage and we will probably deprecate such +/// configuration at a future date. +/// +/// You can also initialize the [NoosphereContext] with an optional third +/// argument: a URL string that refers to a Noosphere Gateway API somewhere +/// on the network that one or more local spheres may have access to. +pub fn ns_initialize( + global_storage_path: char_p::Ref<'_>, + sphere_storage_path: char_p::Ref<'_>, + gateway_url: Option>, +) -> repr_c::Box { + repr_c::Box::new( + NsNoosphereContext::new( + global_storage_path.to_str(), + sphere_storage_path.to_str(), + gateway_url + .map(|value| Url::parse(value.to_str()).unwrap()) + .as_ref(), + ) + .unwrap(), + ) +} + +#[ffi_export] +/// De-allocate a [NoosphereContext]. Note that this will also drop every +/// [SphereContext] that remains active within the [NoosphereContext]. +pub fn ns_free(noosphere: repr_c::Box) { + drop(noosphere) +} diff --git a/rust/noosphere/src/ffi/sphere.rs b/rust/noosphere/src/ffi/sphere.rs new file mode 100644 index 000000000..2b86319ac --- /dev/null +++ b/rust/noosphere/src/ffi/sphere.rs @@ -0,0 +1,77 @@ +use cid::Cid; +use noosphere_core::authority::Authorization; +use safer_ffi::prelude::*; + +use crate::ffi::NsNoosphereContext; +use crate::sphere::SphereReceipt; + +ReprC! { + #[ReprC::opaque] + pub struct NsSphereReceipt { + inner: SphereReceipt + } +} + +impl From for NsSphereReceipt { + fn from(inner: SphereReceipt) -> Self { + NsSphereReceipt { inner } + } +} + +#[ffi_export] +/// Read the sphere identity (a DID encoded as a UTF-8 string) from a +/// [SphereReceipt] +pub fn ns_sphere_receipt_identity<'a>( + sphere_receipt: &'a repr_c::Box, +) -> char_p::Ref<'a> { + char_p::Ref::try_from(sphere_receipt.inner.identity.as_str()).unwrap() +} + +#[ffi_export] +/// Read the mnemonic from a [SphereReceipt] +pub fn ns_sphere_receipt_mnemonic<'a>( + sphere_receipt: &'a repr_c::Box, +) -> char_p::Ref<'a> { + char_p::Ref::try_from(sphere_receipt.inner.mnemonic.as_str()).unwrap() +} + +#[ffi_export] +/// De-allocate a [SphereReceipt] +pub fn ns_sphere_receipt_free(sphere_receipt: repr_c::Box) { + drop(sphere_receipt) +} + +#[ffi_export] +/// Initialize a brand new sphere, authorizing the given key to administer it. +/// The returned value is a [SphereReceipt], containing the DID of the sphere +/// and a human-readable mnemonic that can be used to rotate the key authorized +/// to administer the sphere. +pub fn ns_sphere_create( + noosphere: &mut repr_c::Box, + owner_key_name: char_p::Ref<'_>, +) -> repr_c::Box { + repr_c::Box::new( + pollster::block_on(noosphere.inner_mut().create_sphere(owner_key_name.to_str())) + .unwrap() + .into(), + ) +} + +#[ffi_export] +/// Join a sphere by initializing it and configuring it to use the specified +/// key and authorization. The authorization should be provided in the form of +/// a base64-encoded CID v1 string. +pub fn ns_sphere_join( + noosphere: &mut repr_c::Box, + sphere_identity: char_p::Ref<'_>, + local_key_name: char_p::Ref<'_>, + authorization: char_p::Ref<'_>, +) { + let authorization = Authorization::Cid(Cid::try_from(authorization.to_str()).unwrap()); + pollster::block_on(noosphere.inner_mut().join_sphere( + sphere_identity.to_str(), + local_key_name.to_str(), + &authorization, + )) + .unwrap(); +} diff --git a/rust/noosphere/src/key/insecure.rs b/rust/noosphere/src/key/insecure.rs new file mode 100644 index 000000000..dfce89bef --- /dev/null +++ b/rust/noosphere/src/key/insecure.rs @@ -0,0 +1,82 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use noosphere_core::authority::{ + ed25519_key_to_mnemonic, generate_ed25519_key, restore_ed25519_key, +}; +use std::path::PathBuf; +use tokio::fs; +use ucan::crypto::KeyMaterial; +use ucan_key_support::ed25519::Ed25519KeyMaterial; + +use crate::platform::PlatformKeyMaterial; + +use super::KeyStorage; + +/// InsecureKeyStorage is a stand-in key storage mechanism to tide us over until +/// we have full-fledged support for secure key storage using TPMs or similar +/// hardware. +/// +/// ⚠️ This storage mechanism keeps both public and private key data +/// stored in clear text on disk. User beware! +pub struct InsecureKeyStorage { + storage_path: PathBuf, +} + +impl InsecureKeyStorage { + pub fn new(global_storage_path: &PathBuf) -> Result { + let storage_path = global_storage_path.join("keys"); + + std::fs::create_dir_all(&storage_path)?; + + Ok(InsecureKeyStorage { storage_path }) + } + + fn public_key_path(&self, name: &str) -> PathBuf { + self.storage_path.join(name).with_extension("public") + } + + fn private_key_path(&self, name: &str) -> PathBuf { + self.storage_path.join(name).with_extension("private") + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl KeyStorage for InsecureKeyStorage { + async fn require_key(&self, name: &str) -> Result { + match self.read_key(name).await? { + Some(key) => Ok(key), + None => Err(anyhow!("No key named {} found!", name)), + } + } + + async fn read_key(&self, name: &str) -> Result> { + let private_key_path = self.private_key_path(name); + + if !private_key_path.exists() { + return Ok(None); + } + + let mnemonic = fs::read_to_string(private_key_path).await?; + let key_pair = restore_ed25519_key(&mnemonic)?; + + Ok(Some(key_pair)) + } + + async fn create_key(&self, name: &str) -> Result { + if let Some(key_pair) = self.read_key(name).await? { + return Ok(key_pair); + } + + let key_pair = generate_ed25519_key(); + let mnemonic = ed25519_key_to_mnemonic(&key_pair)?; + let did = key_pair.get_did().await?; + + tokio::try_join!( + fs::write(self.private_key_path(name), mnemonic), + fs::write(self.public_key_path(name), did) + )?; + + Ok(key_pair) + } +} diff --git a/rust/noosphere/src/key/interface.rs b/rust/noosphere/src/key/interface.rs new file mode 100644 index 000000000..024ee8538 --- /dev/null +++ b/rust/noosphere/src/key/interface.rs @@ -0,0 +1,24 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use ucan::crypto::KeyMaterial; + +/// A trait that represents access to arbitrary key storage backends. +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +pub trait KeyStorage +where + K: KeyMaterial, +{ + /// Read a key by name from key storage. + async fn read_key(&self, name: &str) -> Result>; + /// Read a key by name from key storage, but return an error if no key is + /// found by that name. + async fn require_key(&self, name: &str) -> Result { + match self.read_key(name).await? { + Some(key) => Ok(key), + None => Err(anyhow!("No key named {} found!", name)), + } + } + /// Create a key associated with the given name in key storage. + async fn create_key(&self, name: &str) -> Result; +} diff --git a/rust/noosphere/src/key/mod.rs b/rust/noosphere/src/key/mod.rs new file mode 100644 index 000000000..4d6b9c503 --- /dev/null +++ b/rust/noosphere/src/key/mod.rs @@ -0,0 +1,17 @@ +///! Key management is a critical part of working with the Noosphere protocol. +///! This module offers various backing storage mechanisms for key storage, +///! including both insecure and secure options. +mod interface; +pub use interface::*; + +#[cfg(not(target_arch = "wasm32"))] +mod insecure; + +#[cfg(not(target_arch = "wasm32"))] +pub use insecure::InsecureKeyStorage; + +#[cfg(target_arch = "wasm32")] +mod web; + +#[cfg(target_arch = "wasm32")] +pub use web::WebCryptoKeyStorage; diff --git a/rust/noosphere/src/key/web.rs b/rust/noosphere/src/key/web.rs new file mode 100644 index 000000000..ff92c05c9 --- /dev/null +++ b/rust/noosphere/src/key/web.rs @@ -0,0 +1,139 @@ +use std::rc::Rc; + +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use rexie::{KeyRange, ObjectStore, Rexie, RexieBuilder, Store, Transaction, TransactionMode}; +use ucan_key_support::web_crypto::WebCryptoRsaKeyMaterial; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::CryptoKey; + +use super::KeyStorage; + +/// An implementation of key storage backed by the Web Crypto and IndexedDB +/// APIs. This implementation is more secure than storing keys in clear text, +/// but doesn't strictly guarantee that a key is ultimately stored in some +/// kind of hardware-backed secure storage. +pub struct WebCryptoKeyStorage { + db: Rc, +} + +pub const INDEXEDDB_STORAGE_VERSION: u32 = 1; +pub const STORE_NAME: &str = "keys"; + +impl WebCryptoKeyStorage { + pub async fn new(db_name: &str) -> Result { + Self::configure(INDEXEDDB_STORAGE_VERSION, db_name, &[STORE_NAME]).await + } + + async fn configure(version: u32, db_name: &str, store_names: &[&str]) -> Result { + let mut builder = RexieBuilder::new(db_name).version(version); + + for name in store_names { + builder = builder.add_object_store(ObjectStore::new(name).auto_increment(false)); + } + + let db = builder + .build() + .await + .map_err(|error| anyhow!("{:?}", error))?; + + Ok(WebCryptoKeyStorage { db: Rc::new(db) }) + } + + fn start_transaction(&self, mode: TransactionMode) -> Result<(Store, Transaction)> { + let tx = self + .db + .transaction(&[STORE_NAME], mode) + .map_err(|error| anyhow!("{:?}", error))?; + let store = tx + .store(STORE_NAME) + .map_err(|error| anyhow!("{:?}", error))?; + + Ok((store, tx)) + } + + async fn finish_transaction(tx: Transaction) -> Result<()> { + tx.done().await.map_err(|error| anyhow!("{:?}", error))?; + Ok(()) + } + + async fn contains(key: &JsValue, store: &Store) -> Result { + let count = store + .count(Some( + &KeyRange::only(key).map_err(|error| anyhow!("{:?}", error))?, + )) + .await + .map_err(|error| anyhow!("{:?}", error))?; + Ok(count > 0) + } + + async fn read(key: &JsValue, store: &Store) -> Result> { + Ok(match Self::contains(&key, &store).await? { + true => Some( + store + .get(&key) + .await + .map_err(|error| anyhow!("{:?}", error))? + .dyn_into::() + .map_err(|error| anyhow!("{:?}", error))?, + ), + false => None, + }) + } +} + +#[async_trait(?Send)] +impl KeyStorage for WebCryptoKeyStorage { + async fn read_key(&self, name: &str) -> Result> { + let (store, tx) = self.start_transaction(TransactionMode::ReadWrite)?; + + let private_key_name = JsValue::from_str(&format!("{}/private", name)); + let public_key_name = JsValue::from_str(&format!("{}/public", name)); + + let private_key = match WebCryptoKeyStorage::read(&private_key_name, &store).await? { + Some(key) => key, + None => return Ok(None), + }; + let public_key = match WebCryptoKeyStorage::read(&public_key_name, &store).await? { + Some(key) => key, + None => return Ok(None), + }; + + WebCryptoKeyStorage::finish_transaction(tx).await?; + + Ok(Some(WebCryptoRsaKeyMaterial(public_key, Some(private_key)))) + } + + async fn create_key(&self, name: &str) -> Result { + let key_material = WebCryptoRsaKeyMaterial::generate(None).await?; + let (store, tx) = self.start_transaction(TransactionMode::ReadWrite)?; + + let private_key_name = JsValue::from_str(&format!("{}/private", name)); + let public_key_name = JsValue::from_str(&format!("{}/public", name)); + + if WebCryptoKeyStorage::contains(&private_key_name, &store).await? { + return Err(anyhow!("Key name already exists!")); + } + + let private_key = key_material + .1 + .as_ref() + .ok_or_else(|| anyhow!("No private key generated!"))?; + + let public_key = &key_material.0; + + store + .put(&JsValue::from(private_key), Some(&private_key_name)) + .await + .map_err(|error| anyhow!("{:?}", error))?; + + store + .put(&JsValue::from(public_key), Some(&public_key_name)) + .await + .map_err(|error| anyhow!("{:?}", error))?; + + WebCryptoKeyStorage::finish_transaction(tx).await?; + + Ok(key_material) + } +} diff --git a/rust/noosphere/src/lib.rs b/rust/noosphere/src/lib.rs index 6c226ed3f..7b832f75f 100644 --- a/rust/noosphere/src/lib.rs +++ b/rust/noosphere/src/lib.rs @@ -1,7 +1,14 @@ -// pub mod builder; +#[macro_use] +extern crate tracing; + pub mod error; #[cfg(not(target_arch = "wasm32"))] pub mod ffi; +pub mod key; + +mod noosphere; +pub use crate::noosphere::*; + pub mod platform; pub mod sphere; diff --git a/rust/noosphere/src/noosphere.rs b/rust/noosphere/src/noosphere.rs new file mode 100644 index 000000000..81f55eacd --- /dev/null +++ b/rust/noosphere/src/noosphere.rs @@ -0,0 +1,198 @@ +use anyhow::{anyhow, Result}; +use noosphere_core::authority::Authorization; +use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; + +use tokio::sync::Mutex; +use url::Url; + +use crate::{ + key::KeyStorage, + platform::{PlatformKeyMaterial, PlatformKeyStorage, PlatformStore}, + sphere::{SphereContext, SphereContextBuilder, SphereReceipt}, +}; + +/// This enum exists so that we can incrementally layer on support for secure +/// key storage over time. Each member represents a set of environmental +/// qualities, with the most basic represnting an environment with no trusted +/// hardware key storage. +#[derive(Clone)] +pub enum NoosphereContextConfiguration { + /// Insecure configuration should be used on a platform where no TPM or + /// similar secure key storage is available. + Insecure { + global_storage_path: PathBuf, + sphere_storage_path: PathBuf, + gateway_url: Option, + }, + + /// Opaque security configuration may be used in the case where there is + /// some kind of protected keyring-like API layer where secret key material + /// may be considered safely stored. For example: secret service on Linux + /// or the keyring on MacOS. + OpaqueSecurity { + sphere_storage_path: PathBuf, + gateway_url: Option, + }, +} + +/// A [NoosphereContext] holds configuration necessary to initialize and store +/// Noosphere data. It also keeps a running list of active [SphereContext] +/// instances to avoid the expensive action of repeatedly opening and closing +/// a handle to backing storage for spheres that are being accessed regularly. +pub struct NoosphereContext { + configuration: NoosphereContextConfiguration, + sphere_contexts: + Arc>>>>>, +} + +impl NoosphereContext { + /// Initialize a [NoosphereContext] with a [NoosphereContextConfiguration] + pub fn new(configuration: NoosphereContextConfiguration) -> Result { + Ok(NoosphereContext { + configuration, + sphere_contexts: Default::default(), + }) + } + + async fn key_storage(&self) -> Result { + #[cfg(target_arch = "wasm32")] + { + match &self.configuration { + NoosphereContextConfiguration::OpaqueSecurity { .. } => { + PlatformKeyStorage::new("noosphere-keys").await + } + _ => return Err(anyhow!("Unsupported configuration!")), + } + } + + #[cfg(not(target_arch = "wasm32"))] + match &self.configuration { + NoosphereContextConfiguration::Insecure { + global_storage_path, + .. + } => PlatformKeyStorage::new(global_storage_path), + _ => Err(anyhow!("Unsupported configuration!")), + } + } + + fn sphere_storage_path(&self) -> &PathBuf { + match &self.configuration { + NoosphereContextConfiguration::Insecure { + sphere_storage_path, + .. + } => sphere_storage_path, + NoosphereContextConfiguration::OpaqueSecurity { + sphere_storage_path, + .. + } => sphere_storage_path, + } + } + + fn gateway_url(&self) -> Option<&Url> { + match &self.configuration { + NoosphereContextConfiguration::Insecure { gateway_url, .. } => gateway_url.as_ref(), + NoosphereContextConfiguration::OpaqueSecurity { gateway_url, .. } => { + gateway_url.as_ref() + } + } + } + + /// Create a key in the locally available platform key storage, associating + /// it with the given human-readable key name + pub async fn create_key(&self, key_name: &str) -> Result<()> { + let key_storage = self.key_storage().await?; + key_storage.create_key(key_name).await?; + Ok(()) + } + + /// Create a sphere, generating an authorization for the specified owner key + /// to administer the sphere over time + pub async fn create_sphere(&self, owner_key_name: &str) -> Result { + let artifacts = SphereContextBuilder::default() + .create_sphere() + .at_storage_path(self.sphere_storage_path()) + .using_scoped_storage_layout() + .reading_keys_from(self.key_storage().await?) + .using_key(owner_key_name) + .syncing_to(self.gateway_url()) + .build() + .await?; + + let mnemonic = artifacts.require_mnemonic()?.to_owned(); + let context = SphereContext::from(artifacts); + + let sphere_identity = context.identity().to_owned(); + let mut sphere_contexts = self.sphere_contexts.lock().await; + sphere_contexts.insert(sphere_identity.clone(), Arc::new(Mutex::new(context))); + + Ok(SphereReceipt { + identity: sphere_identity, + mnemonic, + }) + } + + /// Join a sphere by DID identity, given a local key and an [Authorization] + /// proving that the key may operate on the sphere. This action will + /// initalize the local sphere workspace, but none of the sphere data will + /// be available until the local application syncs with a gateway that has + /// the sphere data. + pub async fn join_sphere( + &self, + sphere_identity: &str, + local_key_name: &str, + authorization: &Authorization, + ) -> Result<()> { + let artifacts = SphereContextBuilder::default() + .join_sphere(sphere_identity) + .at_storage_path(self.sphere_storage_path()) + .using_scoped_storage_layout() + .reading_keys_from(self.key_storage().await?) + .using_key(local_key_name) + .authorized_by(authorization) + .syncing_to(self.gateway_url()) + .build() + .await?; + + let context = SphereContext::from(artifacts); + + let sphere_identity = context.identity().to_owned(); + let mut sphere_contexts = self.sphere_contexts.lock().await; + sphere_contexts.insert(sphere_identity.clone(), Arc::new(Mutex::new(context))); + + Ok(()) + } + + /// Access a [SphereContext] associated with the given sphere DID identity. + /// The sphere must already have been initialized locally (either by + /// creating it or joining one that was created elsewhere). The act of + /// creating or joining will initialize a [SphereContext], but if such a + /// context has not already been initialized, accessing it with this method + /// will cause it to be initialized and a reference kept by this + /// [NoosphereContext]. + pub async fn get_sphere_context( + &self, + sphere_identity: &str, + ) -> Result>>> { + let mut contexts = self.sphere_contexts.lock().await; + + if !contexts.contains_key(sphere_identity) { + let artifacts = SphereContextBuilder::default() + .open_sphere(sphere_identity) + .at_storage_path(self.sphere_storage_path()) + .using_scoped_storage_layout() + .reading_keys_from(self.key_storage().await?) + .syncing_to(self.gateway_url()) + .build() + .await?; + + let context = SphereContext::from(artifacts); + + contexts.insert(sphere_identity.to_owned(), Arc::new(Mutex::new(context))); + } + + Ok(contexts + .get(sphere_identity) + .ok_or_else(|| anyhow!("Context was not initialized!"))? + .clone()) + } +} diff --git a/rust/noosphere/src/platform.rs b/rust/noosphere/src/platform.rs index 38a8bcd4e..e7e371d6c 100644 --- a/rust/noosphere/src/platform.rs +++ b/rust/noosphere/src/platform.rs @@ -1,26 +1,36 @@ ///! Platform-specific types and bindings +///! Platforms will vary in capabilities for things like block storage and +///! secure key management. This module lays out the concrete strategies we will +///! use on a per-platform basis. #[cfg(all( any(target_arch = "aarch64", target_arch = "x86_64"), target_vendor = "apple" ))] mod inner { - use noosphere_storage::native::NativeStore; + use noosphere_storage::native::{NativeStorageProvider, NativeStore}; use ucan_key_support::ed25519::Ed25519KeyMaterial; + use crate::key::InsecureKeyStorage; + // NOTE: This is going to change when we transition to secure key storage // This key material type implies insecure storage on disk pub type PlatformKeyMaterial = Ed25519KeyMaterial; + pub type PlatformKeyStorage = InsecureKeyStorage; pub type PlatformStore = NativeStore; + pub type PlatformStorageProvider = NativeStorageProvider; } #[cfg(target_arch = "wasm32")] mod inner { - use noosphere_storage::web::WebStore; + use crate::key::WebCryptoKeyStorage; + use noosphere_storage::web::{WebStorageProvider, WebStore}; use ucan_key_support::web_crypto::WebCryptoRsaKeyMaterial; pub type PlatformKeyMaterial = WebCryptoRsaKeyMaterial; + pub type PlatformKeyStorage = WebCryptoKeyStorage; pub type PlatformStore = WebStore; + pub type PlatformStorageProvider = WebStorageProvider; } #[cfg(all( @@ -31,11 +41,15 @@ mod inner { )) ))] mod inner { - use noosphere_storage::native::NativeStore; + use noosphere_storage::native::{NativeStorageProvider, NativeStore}; use ucan_key_support::ed25519::Ed25519KeyMaterial; + use crate::key::InsecureKeyStorage; + pub type PlatformKeyMaterial = Ed25519KeyMaterial; + pub type PlatformKeyStorage = InsecureKeyStorage; pub type PlatformStore = NativeStore; + pub type PlatformStorageProvider = NativeStorageProvider; } pub use inner::*; diff --git a/rust/noosphere/src/sphere.rs b/rust/noosphere/src/sphere.rs deleted file mode 100644 index 50fdb7785..000000000 --- a/rust/noosphere/src/sphere.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::{collections::BTreeMap, sync::Arc}; - -use anyhow::{anyhow, Result}; -use noosphere_api::{ - client::Client, - data::{FetchParameters, FetchResponse}, -}; -use noosphere_core::{authority::Authorization, view::Sphere}; -use noosphere_fs::SphereFs; -use noosphere_storage::{db::SphereDb, interface::Store}; -use ucan::crypto::KeyMaterial; - -use crate::error::NoosphereError; - -pub enum SphereAccess { - ReadOnly, - ReadWrite { - user_key: K, - user_identity: String, - authorization: Authorization, - }, -} - -pub enum SphereNetwork<'a, K, S> -where - K: KeyMaterial, - S: Store, -{ - Online { - client: Arc>>, - }, - Offline, -} - -pub struct SphereContext<'a, K, S> -where - K: KeyMaterial, - S: Store, -{ - user_identity: String, - sphere_identity: String, - access: SphereAccess, - db: SphereDb, - network: SphereNetwork<'a, K, S>, -} - -impl<'a, K, S> SphereContext<'a, K, S> -where - K: KeyMaterial, - S: Store, -{ - pub async fn fs(&self) -> Result, NoosphereError> { - let author_identity = match &self.access { - SphereAccess::ReadOnly => None, - SphereAccess::ReadWrite { user_identity, .. } => Some(user_identity.as_str()), - }; - - SphereFs::latest(&self.sphere_identity, author_identity, &self.db) - .await - .map_err(|e| e.into()) - } - - fn require_online(&self) -> Result>>, NoosphereError> { - match &self.network { - SphereNetwork::Online { client } => Ok(client.clone()), - SphereNetwork::Offline => Err(NoosphereError::NetworkOffline), - } - } -} diff --git a/rust/noosphere/src/sphere/access.rs b/rust/noosphere/src/sphere/access.rs new file mode 100644 index 000000000..e14957027 --- /dev/null +++ b/rust/noosphere/src/sphere/access.rs @@ -0,0 +1,33 @@ +use anyhow::Result; +use noosphere_core::authority::Authorization; +use ucan::crypto::KeyMaterial; + +/// State that represents the form of access the application's user has to a +/// given sphere. +#[derive(Clone)] +pub enum SphereAccess +where + K: KeyMaterial + Clone + 'static, +{ + ReadOnly, + ReadWrite { + user_key: K, + user_identity: String, + authorization: Authorization, + }, +} + +impl SphereAccess +where + K: KeyMaterial + Clone + 'static, +{ + pub async fn read_write(user_key: K, authorization: Authorization) -> Result { + let user_identity = user_key.get_did().await?; + + Ok(SphereAccess::ReadWrite { + user_key, + user_identity, + authorization, + }) + } +} diff --git a/rust/noosphere/src/sphere/builder.rs b/rust/noosphere/src/sphere/builder.rs new file mode 100644 index 000000000..e591ff29a --- /dev/null +++ b/rust/noosphere/src/sphere/builder.rs @@ -0,0 +1,305 @@ +use std::{path::PathBuf, sync::Arc}; + +use anyhow::{anyhow, Result}; +use cid::Cid; + +use noosphere_core::{authority::Authorization, view::Sphere}; + +use noosphere_storage::{db::SphereDb, interface::KeyValueStore, memory::MemoryStore}; +use ucan::crypto::KeyMaterial; +use url::Url; + +use crate::{ + key::KeyStorage, + platform::{PlatformKeyMaterial, PlatformKeyStorage, PlatformStore}, + sphere::{ + access::SphereAccess, + context::SphereContext, + storage::{StorageLayout, AUTHORIZATION, USER_KEY_NAME}, + }, +}; + +enum SphereInitialization { + Create, + Join(String), + Open(String), +} + +impl Default for SphereInitialization { + fn default() -> Self { + SphereInitialization::Create + } +} + +/// The effect of building a [SphereContext] with a [SphereContextBuilder] may +/// include artifacts besides the [SphereContext] that are relevant to the +/// workflow of the API user. This enum encapsulates the various results that +/// are possible. +pub enum SphereContextBuilderArtifacts { + SphereCreated { + context: SphereContext, + mnemonic: String, + }, + SphereOpened(SphereContext), +} + +impl SphereContextBuilderArtifacts { + pub fn require_mnemonic(&self) -> Result<&str> { + match self { + SphereContextBuilderArtifacts::SphereCreated { mnemonic, .. } => Ok(mnemonic.as_str()), + _ => Err(anyhow!( + "The sphere builder artifacts do not include a mnemonic!" + )), + } + } +} + +impl From for SphereContext { + fn from(artifacts: SphereContextBuilderArtifacts) -> Self { + match artifacts { + SphereContextBuilderArtifacts::SphereCreated { context, .. } => context, + SphereContextBuilderArtifacts::SphereOpened(context) => context, + } + } +} + +/// A [SphereContextBuilder] is a common entrypoint for initializing a +/// [SphereContext]. It embodies various workflows that may result in a sphere +/// being activated for use by the embedding application, including: creating a +/// new sphere, joining an existing sphere or accessing a sphere that has +/// already been created or joined. +pub struct SphereContextBuilder { + initialization: SphereInitialization, + scoped_storage_layout: bool, + gateway_url: Option, + storage_path: Option, + authorization: Option, + key_storage: Option, + key_name: Option, +} + +impl SphereContextBuilder { + /// Configure this builder to join a sphere by some DID identity + pub fn join_sphere(mut self, sphere_identity: &str) -> Self { + self.initialization = SphereInitialization::Join(sphere_identity.into()); + self + } + + /// Configure this builder to create a new sphere + pub fn create_sphere(mut self) -> Self { + self.initialization = SphereInitialization::Create; + self + } + + /// Configure this builder to open an existing sphere that was previously + /// created or joined + pub fn open_sphere(mut self, sphere_identity: &str) -> Self { + self.initialization = SphereInitialization::Open(sphere_identity.into()); + self + } + + /// Specify the URL of a gateway API for this application to sync sphere + /// data with + pub fn syncing_to(mut self, gateway_url: Option<&Url>) -> Self { + self.gateway_url = gateway_url.cloned(); + self + } + + /// When initializing sphere data, scope the namespace by the sphere's DID + pub fn using_scoped_storage_layout(mut self) -> Self { + self.scoped_storage_layout = true; + self + } + + /// Specify the local namespace in storage where sphere data should be + /// initialized + pub fn at_storage_path(mut self, path: &PathBuf) -> Self { + self.storage_path = Some(path.clone()); + self + } + + /// Specify the authorization that enables a local key to manipulate a + /// sphere + pub fn authorized_by(mut self, authorization: &Authorization) -> Self { + self.authorization = Some(authorization.clone()); + self + } + + /// Specify the key storage backend (a [KeyStorage] implementation) that + /// manages keys on behalf of the local user + pub fn reading_keys_from(mut self, key_storage: PlatformKeyStorage) -> Self { + self.key_storage = Some(key_storage); + self + } + + /// Specify the name that is associated with a user key in a configured + /// [KeyStorage] backend + pub fn using_key(mut self, key_name: &str) -> Self { + self.key_name = Some(key_name.to_owned()); + self + } + + /// Generate [SphereContextBuilderArtifacts] based on the given + /// configuration of the [SphereContextBuilder]. The successful result of + /// invoking this method will always include an activated [SphereContext]. + /// It will also cause a namespace hierarchy and local data that is + /// is associated with a sphere to exist if it doesn't already. So, consider + /// invocations of this API to have side-effects that may need undoing if + /// idempotence is required (e.g., in tests). + pub async fn build(self) -> Result { + let storage_path = match self.storage_path { + Some(storage_path) => storage_path, + None => return Err(anyhow!("No storage path configured!")), + }; + + match self.initialization { + SphereInitialization::Create => { + let key_storage: PlatformKeyStorage = match self.key_storage { + Some(key_storage) => key_storage, + None => return Err(anyhow!("No key storage configured!")), + }; + + let key_name = match self.key_name { + Some(key_name) => key_name, + None => return Err(anyhow!("No key name configured!")), + }; + + if self.authorization.is_some() { + warn!("Creating a new sphere; the configured authorization will be ignored!"); + } + + let owner_key = key_storage.require_key(&key_name).await?; + let owner_did = owner_key.get_did().await?; + + let mut memory_store = MemoryStore::default(); + let (sphere, authorization, mnemonic) = + Sphere::try_generate(&owner_did, &mut memory_store) + .await + .unwrap(); + + let sphere_did = sphere.try_get_identity().await.unwrap(); + + let storage_layout = match self.scoped_storage_layout { + true => StorageLayout::Scoped(storage_path, sphere_did.clone()), + false => StorageLayout::Unscoped(storage_path), + }; + + let storage_provider = storage_layout.to_storage_provider().await?; + + let mut db = SphereDb::new(&storage_provider).await?; + + db.persist(&memory_store).await?; + + db.set_version(&sphere_did, sphere.cid()).await?; + + db.set_key(USER_KEY_NAME, key_name).await?; + db.set_key(AUTHORIZATION, Cid::try_from(&authorization)?) + .await?; + + Ok(SphereContextBuilderArtifacts::SphereCreated { + context: SphereContext::new( + sphere_did, + SphereAccess::ReadWrite { + user_key: Arc::new(owner_key), + user_identity: owner_did.clone(), + authorization, + }, + db, + self.gateway_url, + ), + mnemonic, + }) + } + SphereInitialization::Join(sphere_identity) => { + let key_storage = match self.key_storage { + Some(key_storage) => key_storage, + None => return Err(anyhow!("No key storage configured!")), + }; + + let key_name = match self.key_name { + Some(key_name) => key_name, + None => return Err(anyhow!("No key name configured!")), + }; + + let authorization = match self.authorization { + Some(authorization) => authorization, + None => return Err(anyhow!("No authorization configured!")), + }; + + let user_key = key_storage.require_key(&key_name).await?; + let user_identity = user_key.get_did().await?; + + let storage_layout = match self.scoped_storage_layout { + true => StorageLayout::Scoped(storage_path, sphere_identity.clone()), + false => StorageLayout::Unscoped(storage_path), + }; + + let storage_provider = storage_layout.to_storage_provider().await?; + + let mut db = SphereDb::new(&storage_provider).await?; + + db.set_key(USER_KEY_NAME, key_name).await?; + db.set_key(AUTHORIZATION, Cid::try_from(&authorization)?) + .await?; + + Ok(SphereContextBuilderArtifacts::SphereOpened( + SphereContext::new( + sphere_identity, + SphereAccess::ReadWrite { + user_key: Arc::new(user_key), + user_identity, + authorization, + }, + db, + self.gateway_url, + ), + )) + } + SphereInitialization::Open(sphere_identity) => { + let storage_layout = match self.scoped_storage_layout { + true => StorageLayout::Scoped(storage_path, sphere_identity.clone()), + false => StorageLayout::Unscoped(storage_path), + }; + + let storage_provider = storage_layout.to_storage_provider().await?; + let db = SphereDb::new(&storage_provider).await?; + + let access = match ( + self.key_storage, + db.get_key(USER_KEY_NAME).await? as Option, + db.get_key(AUTHORIZATION).await?, + ) { + (Some(key_storage), Some(user_key_name), Some(cid)) => { + let user_key = key_storage.require_key(&user_key_name).await?; + let user_identity = user_key.get_did().await?; + + SphereAccess::ReadWrite { + user_key: Arc::new(user_key), + user_identity, + authorization: Authorization::Cid(cid), + } + } + _ => SphereAccess::ReadOnly, + }; + + Ok(SphereContextBuilderArtifacts::SphereOpened( + SphereContext::new(sphere_identity, access, db, self.gateway_url), + )) + } + } + } +} + +impl Default for SphereContextBuilder { + fn default() -> Self { + Self { + initialization: SphereInitialization::Create, + scoped_storage_layout: false, + gateway_url: None, + storage_path: None, + authorization: None, + key_storage: None as Option, + key_name: None, + } + } +} diff --git a/rust/noosphere/src/sphere/context.rs b/rust/noosphere/src/sphere/context.rs new file mode 100644 index 000000000..224cdfc77 --- /dev/null +++ b/rust/noosphere/src/sphere/context.rs @@ -0,0 +1,133 @@ +use std::sync::Arc; + +use anyhow::Result; +use noosphere_api::client::Client; + +use noosphere_core::authority::{Authorization, SUPPORTED_KEYS}; +use noosphere_fs::SphereFs; +use noosphere_storage::{db::SphereDb, interface::Store}; +use tokio::sync::OnceCell; +use ucan::crypto::{did::DidParser, KeyMaterial}; +use url::Url; + +use crate::error::NoosphereError; + +use super::access::SphereAccess; + +/// A [SphereContext] is an accessor construct over locally replicated sphere +/// data. It embodies both the storage layer that contains the sphere's data +/// as the information needed to verify a user's intended level of access to +/// it (e.g., local key material and [ucan::Ucan]-based authorization). +/// Additionally, the [SphereContext] maintains a reference to an API [Client] +/// that may be initialized as the network becomes available. +/// +/// All interactions that pertain to a sphere, including reading or writing +/// its contents and syncing with a gateway, flow through the [SphereContext]. +pub struct SphereContext +where + K: KeyMaterial + 'static, + S: Store, +{ + sphere_identity: String, + gateway_url: Option, + access: SphereAccess>, + db: SphereDb, + did_parser: DidParser, + client: OnceCell, SphereDb>>>, +} + +impl SphereContext +where + K: KeyMaterial + 'static, + S: Store, +{ + pub fn new( + sphere_identity: String, + access: SphereAccess>, + db: SphereDb, + gateway_url: Option, + ) -> Self { + SphereContext { + sphere_identity, + access, + db, + gateway_url, + did_parser: DidParser::new(SUPPORTED_KEYS), + client: OnceCell::new(), + } + } + + /// The DID identity of the sphere + pub fn identity(&self) -> &str { + &self.sphere_identity + } + + /// The key authorized to access the sphere in this context + pub fn user_key(&self) -> Option> { + match &self.access { + SphereAccess::ReadOnly => None, + SphereAccess::ReadWrite { user_key, .. } => Some(user_key.clone()), + } + } + + /// The authorization that allows the user key to access the sphere in this context + pub fn user_authorization(&self) -> Option<&Authorization> { + match &self.access { + SphereAccess::ReadOnly => None, + SphereAccess::ReadWrite { authorization, .. } => Some(authorization), + } + } + + /// Get a [SphereFs] instance over the current sphere's content; note that + /// if the user's [SphereAccess] is read-only, the returned [SphereFs] will + /// be read-only as well. + pub async fn fs(&self) -> Result, NoosphereError> { + let author_identity = match &self.access { + SphereAccess::ReadOnly => None, + SphereAccess::ReadWrite { user_identity, .. } => Some(user_identity.as_str()), + }; + + SphereFs::latest(&self.sphere_identity, author_identity, &self.db) + .await + .map_err(|e| e.into()) + } + + /// Get a [Client] that will interact with a configured gateway (if a URL + /// for one has been configured). This will initialize a [Client] if one is + /// not already intialized, and will fail if the [Client] is unable to + /// verify the identity of the gateway or otherwise connect to it. + pub async fn client(&mut self) -> Result, SphereDb>>, NoosphereError> { + let client = self + .client + .get_or_try_init(|| async { + let gateway_url = self + .gateway_url + .clone() + .ok_or(NoosphereError::MissingConfiguration("gateway URL"))?; + + let (credential, authorization) = match &self.access { + SphereAccess::ReadOnly => return Err(NoosphereError::NoCredentials), + SphereAccess::ReadWrite { + user_key, + authorization, + .. + } => (user_key.clone(), authorization.clone()), + }; + + Ok(Arc::new( + Client::identify( + &self.sphere_identity, + &gateway_url, + credential, + &authorization, + &mut self.did_parser, + self.db.clone(), + ) + .await?, + )) + }) + .await?; + + Ok(client.clone()) + } +} diff --git a/rust/noosphere/src/sphere/mod.rs b/rust/noosphere/src/sphere/mod.rs new file mode 100644 index 000000000..5cdc01831 --- /dev/null +++ b/rust/noosphere/src/sphere/mod.rs @@ -0,0 +1,11 @@ +mod access; +mod builder; +mod context; +mod receipt; +mod storage; + +pub use access::*; +pub use builder::*; +pub use context::*; +pub use receipt::*; +pub use storage::*; diff --git a/rust/noosphere/src/sphere/receipt.rs b/rust/noosphere/src/sphere/receipt.rs new file mode 100644 index 000000000..441af76d7 --- /dev/null +++ b/rust/noosphere/src/sphere/receipt.rs @@ -0,0 +1,4 @@ +pub struct SphereReceipt { + pub identity: String, + pub mnemonic: String, +} diff --git a/rust/noosphere/src/sphere/storage.rs b/rust/noosphere/src/sphere/storage.rs new file mode 100644 index 000000000..d44997619 --- /dev/null +++ b/rust/noosphere/src/sphere/storage.rs @@ -0,0 +1,56 @@ +use std::{fmt::Display, path::PathBuf}; + +use crate::platform::PlatformStorageProvider; +use anyhow::Result; + +pub const USER_KEY_NAME: &str = "user_key_name"; +pub const AUTHORIZATION: &str = "authorization"; + +/// [StorageLayout] represents the namespace that should be used depending on +/// whether or not a sphere's DID should be included in the namespace. The enum +/// is a convenience that can be directly transformed into a +/// [noosphere_storage::interface::StorageProvider] implementation that is +/// suitable for the current platform +pub enum StorageLayout { + Scoped(PathBuf, String), + Unscoped(PathBuf), +} + +impl Display for StorageLayout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let path = PathBuf::from(self); + + write!(f, "{}", path.to_string_lossy()) + } +} + +impl From<&StorageLayout> for PathBuf { + fn from(layout: &StorageLayout) -> Self { + match layout { + StorageLayout::Scoped(path, scope) => path.join(scope), + StorageLayout::Unscoped(path) => path.clone(), + } + } +} + +impl From for PathBuf { + fn from(layout: StorageLayout) -> Self { + PathBuf::from(&layout) + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl StorageLayout { + pub async fn to_storage_provider(&self) -> Result { + PlatformStorageProvider::new(noosphere_storage::native::NativeStorageInit::Path( + PathBuf::from(self), + )) + } +} + +#[cfg(target_arch = "wasm32")] +impl StorageLayout { + pub async fn to_storage_provider(&self) -> Result { + PlatformStorageProvider::new(&self.to_string()).await + } +} diff --git a/rust/noosphere/tests/integration.rs b/rust/noosphere/tests/integration.rs new file mode 100644 index 000000000..c41999549 --- /dev/null +++ b/rust/noosphere/tests/integration.rs @@ -0,0 +1,114 @@ +#![cfg(test)] + +use noosphere_core::data::ContentType; +use tokio::io::AsyncReadExt; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::wasm_bindgen_test; + +#[cfg(target_arch = "wasm32")] +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +use noosphere::{sphere::SphereReceipt, NoosphereContext, NoosphereContextConfiguration}; + +#[cfg(target_arch = "wasm32")] +fn platform_configuration() -> NoosphereContextConfiguration { + NoosphereContextConfiguration::OpaqueSecurity { + sphere_storage_path: "sphere-data".into(), + gateway_url: None, + } +} + +#[cfg(not(target_arch = "wasm32"))] +fn platform_configuration() -> NoosphereContextConfiguration { + use temp_dir::TempDir; + + let global_storage_path = TempDir::new().unwrap().path().to_path_buf(); + let sphere_storage_path = TempDir::new().unwrap().path().to_path_buf(); + + NoosphereContextConfiguration::Insecure { + global_storage_path: global_storage_path, + sphere_storage_path: sphere_storage_path, + gateway_url: None, + } +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn single_player_single_device_end_to_end_workflow() { + #[cfg(target_arch = "wasm32")] + tracing_wasm::set_as_global_default(); + + let configuration = platform_configuration(); + let key_name = "foobar"; + + // Create the sphere and write a file to it + let sphere_identity = { + let noosphere = NoosphereContext::new(configuration.clone()).unwrap(); + + noosphere.create_key(key_name).await.unwrap(); + + let SphereReceipt { + identity: sphere_identity, + .. + } = noosphere.create_sphere(key_name).await.unwrap(); + + let sphere_context = noosphere + .get_sphere_context(&sphere_identity) + .await + .unwrap(); + let sphere_context = sphere_context.lock().await; + + let mut fs = sphere_context.fs().await.unwrap(); + + fs.write("foo", "text/plain", b"bar".as_ref(), None) + .await + .unwrap(); + + fs.save( + &sphere_context.user_key().unwrap(), + sphere_context.user_authorization(), + None, + ) + .await + .unwrap(); + + sphere_identity + }; + + // Open the sphere later and read the file and write another file + { + let noosphere = NoosphereContext::new(configuration.clone()).unwrap(); + + let sphere_context = noosphere + .get_sphere_context(&sphere_identity) + .await + .unwrap(); + let sphere_context = sphere_context.lock().await; + + let mut fs = sphere_context.fs().await.unwrap(); + + let mut file = fs.read("foo").await.unwrap().unwrap(); + + assert_eq!( + file.memo.content_type(), + Some(ContentType::Unknown("text/plain".into())) + ); + + let mut contents = String::new(); + file.contents.read_to_string(&mut contents).await.unwrap(); + + assert_eq!(contents, "bar"); + + fs.write("cats", "text/subtext", b"are great".as_ref(), None) + .await + .unwrap(); + + fs.save( + &sphere_context.user_key().unwrap(), + sphere_context.user_authorization(), + None, + ) + .await + .unwrap(); + }; +} diff --git a/swift/Sources/SwiftNoosphere/Noosphere.swift b/swift/Sources/SwiftNoosphere/Noosphere.swift new file mode 100644 index 000000000..8c0eaad1e --- /dev/null +++ b/swift/Sources/SwiftNoosphere/Noosphere.swift @@ -0,0 +1 @@ +@_exported import Noosphere diff --git a/swift/Tests/SwiftNoosphereTests/NoosphereTests.swift b/swift/Tests/SwiftNoosphereTests/NoosphereTests.swift new file mode 100644 index 000000000..4c098985a --- /dev/null +++ b/swift/Tests/SwiftNoosphereTests/NoosphereTests.swift @@ -0,0 +1,12 @@ +import XCTest +@testable import SwiftNoosphere + +final class NoosphereTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + // TODO + noosphere_key_create("foobar") + } +} \ No newline at end of file