Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/bitwarden-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ wasm = [
] # WASM support

[dependencies]
async-trait = { workspace = true }
base64 = ">=0.22.1, <0.23"
bitwarden-api-api = { workspace = true }
bitwarden-api-identity = { workspace = true }
Expand Down
36 changes: 32 additions & 4 deletions crates/bitwarden-core/src/auth/renew.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::sync::Arc;

use chrono::Utc;

use super::login::LoginError;
Expand All @@ -10,18 +12,44 @@
};
use crate::{
auth::api::{request::ApiTokenRequest, response::IdentityTokenResponse},
client::{internal::InternalClient, LoginMethod, UserLoginMethod},
client::{
internal::{ClientManagedTokens, InternalClient, SdkManagedTokens, Tokens},
LoginMethod, UserLoginMethod,
},
NotAuthenticatedError,
};

pub(crate) async fn renew_token(client: &InternalClient) -> Result<(), LoginError> {
const TOKEN_RENEW_MARGIN_SECONDS: i64 = 5 * 60;

let tokens = client
let tokens_guard = client
.tokens
.read()
.expect("RwLock is not poisoned")
.clone();

match tokens_guard {
Tokens::SdkManaged(tokens) => renew_token_sdk_managed(client, tokens).await,
Tokens::ClientManaged(tokens) => renew_token_client_managed(client, tokens).await,

Check warning on line 31 in crates/bitwarden-core/src/auth/renew.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-core/src/auth/renew.rs#L31

Added line #L31 was not covered by tests
}
}

async fn renew_token_client_managed(
client: &InternalClient,
tokens: Arc<dyn ClientManagedTokens>,
) -> Result<(), LoginError> {
let token = tokens
.get_access_token()
.await
.ok_or(NotAuthenticatedError)?;
client.set_tokens_internal(token);
Ok(())
}

Check warning on line 45 in crates/bitwarden-core/src/auth/renew.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-core/src/auth/renew.rs#L35-L45

Added lines #L35 - L45 were not covered by tests

async fn renew_token_sdk_managed(
client: &InternalClient,
tokens: SdkManagedTokens,

Check warning on line 49 in crates/bitwarden-core/src/auth/renew.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-core/src/auth/renew.rs#L47-L49

Added lines #L47 - L49 were not covered by tests
) -> Result<(), LoginError> {
const TOKEN_RENEW_MARGIN_SECONDS: i64 = 5 * 60;

let login_method = client
.login_method
.read()
Expand Down
18 changes: 15 additions & 3 deletions crates/bitwarden-core/src/client/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use crate::client::flags::Flags;
use crate::client::{
client_settings::ClientSettings,
internal::{ApiConfigurations, Tokens},
internal::{ApiConfigurations, ClientManagedTokens, SdkManagedTokens, Tokens},
};

/// The main struct to interact with the Bitwarden SDK.
Expand All @@ -26,7 +26,19 @@

impl Client {
#[allow(missing_docs)]
pub fn new(settings_input: Option<ClientSettings>) -> Self {
pub fn new(settings: Option<ClientSettings>) -> Self {
Self::new_tokens(settings, Tokens::SdkManaged(SdkManagedTokens::default()))
}

#[allow(missing_docs)]
pub fn new_with_client_tokens(
settings: Option<ClientSettings>,
tokens: Arc<dyn ClientManagedTokens>,
) -> Self {
Self::new_tokens(settings, Tokens::ClientManaged(tokens))
}

Check warning on line 39 in crates/bitwarden-core/src/client/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-core/src/client/client.rs#L34-L39

Added lines #L34 - L39 were not covered by tests

fn new_tokens(settings_input: Option<ClientSettings>, tokens: Tokens) -> Self {
let settings = settings_input.unwrap_or_default();

fn new_client_builder() -> reqwest::ClientBuilder {
Expand Down Expand Up @@ -81,7 +93,7 @@
Self {
internal: Arc::new(InternalClient {
user_id: OnceLock::new(),
tokens: RwLock::new(Tokens::default()),
tokens: RwLock::new(tokens),
login_method: RwLock::new(None),
#[cfg(feature = "internal")]
flags: RwLock::new(Flags::default()),
Expand Down
53 changes: 28 additions & 25 deletions crates/bitwarden-core/src/client/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,26 @@ pub struct ApiConfigurations {
pub device_type: DeviceType,
}

/// Access and refresh tokens used for authentication and authorization.
#[derive(Debug, Clone)]
pub(crate) enum Tokens {
SdkManaged(SdkManagedTokens),
ClientManaged(Arc<dyn ClientManagedTokens>),
}

/// Access tokens managed by client applications, such as the web or mobile apps.
#[async_trait::async_trait]
pub trait ClientManagedTokens: std::fmt::Debug + Send + Sync {
/// Returns the access token, if available.
async fn get_access_token(&self) -> Option<String>;
}

/// Tokens managed by the SDK, the SDK will automatically handle token renewal.
#[derive(Debug, Default, Clone)]
pub(crate) struct Tokens {
pub(crate) struct SdkManagedTokens {
// These two fields are always written to, but they are not read
// from the secrets manager SDK.
#[cfg_attr(not(feature = "internal"), allow(dead_code))]
#[allow(dead_code)]
access_token: Option<String>,
pub(crate) expires_on: Option<i64>,

Expand Down Expand Up @@ -117,11 +132,17 @@ impl InternalClient {
}

pub(crate) fn set_tokens(&self, token: String, refresh_token: Option<String>, expires_in: u64) {
*self.tokens.write().expect("RwLock is not poisoned") = Tokens {
access_token: Some(token.clone()),
expires_on: Some(Utc::now().timestamp() + expires_in as i64),
refresh_token,
};
*self.tokens.write().expect("RwLock is not poisoned") =
Tokens::SdkManaged(SdkManagedTokens {
access_token: Some(token.clone()),
expires_on: Some(Utc::now().timestamp() + expires_in as i64),
refresh_token,
});
self.set_tokens_internal(token);
}

/// Used to set tokens for internal API clients, use [set_tokens] for SdkManagedTokens.
pub(crate) fn set_tokens_internal(&self, token: String) {
let mut guard = self
.__api_configurations
.write()
Expand All @@ -132,24 +153,6 @@ impl InternalClient {
inner.api.oauth_access_token = Some(token);
}

#[allow(missing_docs)]
#[cfg(feature = "internal")]
pub fn is_authed(&self) -> bool {
let is_token_set = self
.tokens
.read()
.expect("RwLock is not poisoned")
.access_token
.is_some();
let is_login_method_set = self
.login_method
.read()
.expect("RwLock is not poisoned")
.is_some();

is_token_set || is_login_method_set
}

#[allow(missing_docs)]
#[cfg(feature = "internal")]
pub fn get_kdf(&self) -> Result<Kdf, NotAuthenticatedError> {
Expand Down
12 changes: 4 additions & 8 deletions crates/bitwarden-core/src/platform/get_user_api_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,10 @@
}

fn get_login_method(client: &Client) -> Result<Arc<LoginMethod>, NotAuthenticatedError> {
if client.internal.is_authed() {
client
.internal
.get_login_method()
.ok_or(NotAuthenticatedError)
} else {
Err(NotAuthenticatedError)
}
client
.internal
.get_login_method()
.ok_or(NotAuthenticatedError)

Check warning on line 69 in crates/bitwarden-core/src/platform/get_user_api_key.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-core/src/platform/get_user_api_key.rs#L66-L69

Added lines #L66 - L69 were not covered by tests
}

/// Build the secret verification request.
Expand Down
4 changes: 3 additions & 1 deletion crates/bitwarden-vault/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ uniffi = [
wasm = [
"bitwarden-core/wasm",
"dep:tsify-next",
"dep:wasm-bindgen"
"dep:wasm-bindgen",
"dep:wasm-bindgen-futures"
] # WASM support

[dependencies]
Expand All @@ -48,6 +49,7 @@ tsify-next = { workspace = true, optional = true }
uniffi = { workspace = true, optional = true }
uuid = { workspace = true }
wasm-bindgen = { workspace = true, optional = true }
wasm-bindgen-futures = { workspace = true, optional = true }

[dev-dependencies]
tokio = { workspace = true, features = ["rt"] }
Expand Down
10 changes: 6 additions & 4 deletions crates/bitwarden-vault/src/folder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@ use {tsify_next::Tsify, wasm_bindgen::prelude::*};
use crate::VaultParseError;

#[allow(missing_docs)]
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct Folder {
id: Option<Uuid>,
name: EncString,
revision_date: DateTime<Utc>,
pub id: Option<Uuid>,
pub name: EncString,
pub revision_date: DateTime<Utc>,
}

bitwarden_state::register_repository_item!(Folder, "Folder");

#[allow(missing_docs)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
Expand Down
91 changes: 89 additions & 2 deletions crates/bitwarden-vault/src/folder_client.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,53 @@
use bitwarden_core::Client;
use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel};
use bitwarden_core::{require, ApiError, Client, MissingFieldError};
use bitwarden_error::bitwarden_error;
use bitwarden_state::repository::RepositoryError;
use chrono::Utc;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[cfg(feature = "wasm")]
use tsify_next::Tsify;
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;

use crate::{
error::{DecryptError, EncryptError},
Folder, FolderView,
Folder, FolderView, VaultParseError,
};

/// Request to add or edit a folder.
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct FolderAddEditRequest {
pub name: String,
}

#[allow(missing_docs)]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub struct FoldersClient {
pub(crate) client: Client,
}

#[allow(missing_docs)]
#[bitwarden_error(flat)]

Check warning on line 34 in crates/bitwarden-vault/src/folder_client.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-vault/src/folder_client.rs#L34

Added line #L34 was not covered by tests
#[derive(Debug, Error)]
pub enum CreateFolderError {
#[error(transparent)]
Encrypt(#[from] EncryptError),
#[error(transparent)]
Decrypt(#[from] DecryptError),
#[error(transparent)]
Api(#[from] ApiError),
#[error(transparent)]
VaultParse(#[from] VaultParseError),
#[error(transparent)]
MissingField(#[from] MissingFieldError),
#[error(transparent)]
RepositoryError(#[from] RepositoryError),
}

#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl FoldersClient {
#[allow(missing_docs)]
Expand All @@ -35,4 +70,56 @@
let views = key_store.decrypt_list(&folders)?;
Ok(views)
}

/// Create a new folder and save it to the server.
pub async fn create(
&self,
request: FolderAddEditRequest,
) -> Result<FolderView, CreateFolderError> {

Check warning on line 78 in crates/bitwarden-vault/src/folder_client.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-vault/src/folder_client.rs#L75-L78

Added lines #L75 - L78 were not covered by tests
// TODO: We should probably not use a Folder model here, but rather create
// FolderRequestModel directly?
let folder = self.encrypt(FolderView {
id: None,
name: request.name,
revision_date: Utc::now(),
})?;

Check warning on line 85 in crates/bitwarden-vault/src/folder_client.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-vault/src/folder_client.rs#L81-L85

Added lines #L81 - L85 were not covered by tests

let config = self.client.internal.get_api_configurations().await;
let resp = folders_api::folders_post(
&config.api,
Some(FolderRequestModel {
name: folder.name.to_string(),
}),
)
.await
.map_err(ApiError::from)?;

Check warning on line 95 in crates/bitwarden-vault/src/folder_client.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-vault/src/folder_client.rs#L87-L95

Added lines #L87 - L95 were not covered by tests

let folder: Folder = resp.try_into()?;

Check warning on line 97 in crates/bitwarden-vault/src/folder_client.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-vault/src/folder_client.rs#L97

Added line #L97 was not covered by tests

self.client
.platform()
.state()
.get_client_managed::<Folder>()
.ok_or(MissingFieldError("Folder not found in state"))?
.set(require!(folder.id).to_string(), folder.clone())
.await?;

Check warning on line 105 in crates/bitwarden-vault/src/folder_client.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-vault/src/folder_client.rs#L99-L105

Added lines #L99 - L105 were not covered by tests

Ok(self.decrypt(folder)?)
}

Check warning on line 108 in crates/bitwarden-vault/src/folder_client.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-vault/src/folder_client.rs#L107-L108

Added lines #L107 - L108 were not covered by tests

/// Edit the folder.
///
/// TODO: Replace `old_folder` with `FolderId` and load the old folder from state.
/// TODO: Save the folder to the server and state.
pub fn edit_without_state(
&self,
old_folder: Folder,
folder: FolderAddEditRequest,
) -> Result<Folder, EncryptError> {
self.encrypt(FolderView {
id: old_folder.id,
name: folder.name,
revision_date: old_folder.revision_date,
})
}

Check warning on line 124 in crates/bitwarden-vault/src/folder_client.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-vault/src/folder_client.rs#L114-L124

Added lines #L114 - L124 were not covered by tests
}
12 changes: 8 additions & 4 deletions crates/bitwarden-wasm-internal/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
extern crate console_error_panic_hook;
use std::fmt::Display;
use std::{fmt::Display, sync::Arc};

use bitwarden_core::{key_management::CryptoClient, Client, ClientSettings};
use bitwarden_error::bitwarden_error;
Expand All @@ -8,7 +8,10 @@
use bitwarden_vault::{VaultClient, VaultClientExt};
use wasm_bindgen::prelude::*;

use crate::platform::PlatformClient;
use crate::platform::{
token_provider::{JsTokenProvider, WasmClientManagedTokens},
PlatformClient,
};

#[allow(missing_docs)]
#[wasm_bindgen]
Expand All @@ -18,8 +21,9 @@
impl BitwardenClient {
#[allow(missing_docs)]
#[wasm_bindgen(constructor)]
pub fn new(settings: Option<ClientSettings>) -> Self {
Self(Client::new(settings))
pub fn new(settings: Option<ClientSettings>, token_provider: JsTokenProvider) -> Self {
let tokens = Arc::new(WasmClientManagedTokens::new(token_provider));
Self(Client::new_with_client_tokens(settings, tokens))

Check warning on line 26 in crates/bitwarden-wasm-internal/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-wasm-internal/src/client.rs#L24-L26

Added lines #L24 - L26 were not covered by tests
}

/// Test method, echoes back the input
Expand Down
Loading
Loading