Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ pub fn edit(
refresh_token: &str,
id: &str,
org_id: Option<&str>,
key: Option<&str>,
name: &str,
data: &crate::db::EntryData,
fields: &[crate::db::Field],
Expand All @@ -210,6 +211,7 @@ pub fn edit(
access_token,
id,
org_id,
key,
name,
data,
fields,
Expand All @@ -224,6 +226,7 @@ fn edit_once(
access_token: &str,
id: &str,
org_id: Option<&str>,
key: Option<&str>,
name: &str,
data: &crate::db::EntryData,
fields: &[crate::db::Field],
Expand All @@ -236,6 +239,7 @@ fn edit_once(
access_token,
id,
org_id,
key,
name,
data,
fields,
Expand Down
3 changes: 3 additions & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,7 @@ struct CiphersPutReq {
folder_id: Option<String>,
#[serde(rename = "organizationId")]
organization_id: Option<String>,
key: Option<String>,
name: String,
notes: Option<String>,
login: Option<CipherLogin>,
Expand Down Expand Up @@ -1322,6 +1323,7 @@ impl Client {
access_token: &str,
id: &str,
org_id: Option<&str>,
key: Option<&str>,
name: &str,
data: &crate::db::EntryData,
fields: &[crate::db::Field],
Expand All @@ -1339,6 +1341,7 @@ impl Client {
},
folder_id: folder_uuid.map(std::string::ToString::to_string),
organization_id: org_id.map(std::string::ToString::to_string),
key: key.map(std::string::ToString::to_string),
name: name.to_string(),
notes: notes.map(std::string::ToString::to_string),
login: None,
Expand Down
97 changes: 87 additions & 10 deletions src/bin/rbw-agent/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,22 +693,46 @@ pub async fn encrypt(
state: std::sync::Arc<tokio::sync::Mutex<crate::state::State>>,
plaintext: &str,
org_id: Option<&str>,
entry_key: Option<&str>,
) -> anyhow::Result<()> {
let state = state.lock().await;
let Some(keys) = state.key(org_id) else {
return Err(anyhow::anyhow!(
"failed to find encryption keys in in-memory state"
));
let keys = state.key(org_id).ok_or_else(|| {
anyhow::anyhow!("failed to find encryption keys in in-memory state")
})?;
let cipherstring = encrypt_with_key(keys, entry_key, plaintext)?;

respond_encrypt(sock, cipherstring).await?;

Ok(())
}

fn encrypt_with_key(
keys: &rbw::locked::Keys,
entry_key: Option<&str>,
plaintext: &str,
) -> anyhow::Result<String> {
let cipher_keys;
let keys = match entry_key {
Some(entry_key) => {
let key_cipherstring = rbw::cipherstring::CipherString::new(
entry_key,
)
.context("failed to parse individual item encryption key")?;
cipher_keys = rbw::locked::Keys::new(
key_cipherstring.decrypt_locked_symmetric(keys).context(
"failed to decrypt individual item encryption key",
)?,
);
&cipher_keys
}
None => keys,
};
let cipherstring = rbw::cipherstring::CipherString::encrypt_symmetric(
rbw::cipherstring::CipherString::encrypt_symmetric(
keys,
plaintext.as_bytes(),
)
.context("failed to encrypt plaintext secret")?;

respond_encrypt(sock, cipherstring.to_string()).await?;

Ok(())
.context("failed to encrypt plaintext secret")
.map(|c| c.to_string())
}

#[cfg(feature = "clipboard")]
Expand Down Expand Up @@ -950,3 +974,56 @@ pub async fn find_ssh_private_key(

Err(anyhow::anyhow!("No matching private key found"))
}

#[cfg(test)]
mod tests {
use super::*;

fn test_keys(seed: u8) -> rbw::locked::Keys {
let mut v = rbw::locked::Vec::new();
v.extend((0..64).map(|i| seed.wrapping_add(i)));
rbw::locked::Keys::new(v)
}

#[test]
fn encrypt_with_item_key_roundtrip() {
let master = test_keys(1);
let item = test_keys(100);

// the wrapped item key as stored in the cipher's `key` field
let mut full_key = item.enc_key().to_vec();
full_key.extend_from_slice(item.mac_key());
let wrapped = rbw::cipherstring::CipherString::encrypt_symmetric(
&master, &full_key,
)
.unwrap()
.to_string();

// encrypt with the item key (the fixed path)
let ct = encrypt_with_key(&master, Some(&wrapped), "secret").unwrap();

// decrypting with the item key succeeds
let plain = rbw::cipherstring::CipherString::new(&ct)
.unwrap()
.decrypt_symmetric(&master, Some(&item))
.unwrap();
assert_eq!(plain, b"secret");

// decrypting with the master key (the old broken behaviour) fails
assert!(rbw::cipherstring::CipherString::new(&ct)
.unwrap()
.decrypt_symmetric(&master, None)
.is_err());
}

#[test]
fn encrypt_without_item_key_uses_master_key() {
let master = test_keys(2);
let ct = encrypt_with_key(&master, None, "secret").unwrap();
let plain = rbw::cipherstring::CipherString::new(&ct)
.unwrap()
.decrypt_symmetric(&master, None)
.unwrap();
assert_eq!(plain, b"secret");
}
}
7 changes: 6 additions & 1 deletion src/bin/rbw-agent/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,17 @@ async fn handle_request(
.await?;
true
}
rbw::protocol::Action::Encrypt { plaintext, org_id } => {
rbw::protocol::Action::Encrypt {
plaintext,
org_id,
entry_key,
} => {
crate::actions::encrypt(
sock,
state.clone(),
plaintext,
org_id.as_deref(),
entry_key.as_deref(),
)
.await?;
true
Expand Down
2 changes: 2 additions & 0 deletions src/bin/rbw/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,15 @@ pub fn decrypt(
pub fn encrypt(
plaintext: &str,
org_id: Option<&str>,
entry_key: Option<&str>,
) -> anyhow::Result<String> {
let mut sock = connect()?;
sock.send(&rbw::protocol::Request::new(
get_environment(),
rbw::protocol::Action::Encrypt {
plaintext: plaintext.to_string(),
org_id: org_id.map(std::string::ToString::to_string),
entry_key: entry_key.map(std::string::ToString::to_string),
},
))?;

Expand Down
36 changes: 23 additions & 13 deletions src/bin/rbw/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1569,26 +1569,26 @@ pub fn add(
let mut access_token = db.access_token.as_ref().unwrap().clone();
let refresh_token = db.refresh_token.as_ref().unwrap();

let name = crate::actions::encrypt(name, None)?;
let name = crate::actions::encrypt(name, None, None)?;

let username = username
.map(|username| crate::actions::encrypt(username, None))
.map(|username| crate::actions::encrypt(username, None, None))
.transpose()?;

let contents = rbw::edit::edit("", HELP_PW)?;

let (password, notes) = parse_editor(&contents);
let password = password
.map(|password| crate::actions::encrypt(&password, None))
.map(|password| crate::actions::encrypt(&password, None, None))
.transpose()?;
let notes = notes
.map(|notes| crate::actions::encrypt(&notes, None))
.map(|notes| crate::actions::encrypt(&notes, None, None))
.transpose()?;
let uris: Vec<_> = uris
.iter()
.map(|uri| {
Ok(rbw::db::Uri {
uri: crate::actions::encrypt(&uri.0, None)?,
uri: crate::actions::encrypt(&uri.0, None, None)?,
match_type: uri.1,
})
})
Expand Down Expand Up @@ -1621,7 +1621,7 @@ pub fn add(
let (new_access_token, id) = rbw::actions::create_folder(
&access_token,
refresh_token,
&crate::actions::encrypt(folder_name, None)?,
&crate::actions::encrypt(folder_name, None, None)?,
)?;
if let Some(new_access_token) = new_access_token {
access_token.clone_from(&new_access_token);
Expand Down Expand Up @@ -1674,16 +1674,16 @@ pub fn generate(
let mut access_token = db.access_token.as_ref().unwrap().clone();
let refresh_token = db.refresh_token.as_ref().unwrap();

let name = crate::actions::encrypt(name, None)?;
let name = crate::actions::encrypt(name, None, None)?;
let username = username
.map(|username| crate::actions::encrypt(username, None))
.map(|username| crate::actions::encrypt(username, None, None))
.transpose()?;
let password = crate::actions::encrypt(&password, None)?;
let password = crate::actions::encrypt(&password, None, None)?;
let uris: Vec<_> = uris
.iter()
.map(|uri| {
Ok(rbw::db::Uri {
uri: crate::actions::encrypt(&uri.0, None)?,
uri: crate::actions::encrypt(&uri.0, None, None)?,
match_type: uri.1,
})
})
Expand Down Expand Up @@ -1716,7 +1716,7 @@ pub fn generate(
let (new_access_token, id) = rbw::actions::create_folder(
&access_token,
refresh_token,
&crate::actions::encrypt(folder_name, None)?,
&crate::actions::encrypt(folder_name, None, None)?,
)?;
if let Some(new_access_token) = new_access_token {
access_token.clone_from(&new_access_token);
Expand Down Expand Up @@ -1788,12 +1788,17 @@ pub fn edit(
crate::actions::encrypt(
&password,
entry.org_id.as_deref(),
entry.key.as_deref(),
)
})
.transpose()?;
let notes = notes
.map(|notes| {
crate::actions::encrypt(&notes, entry.org_id.as_deref())
crate::actions::encrypt(
&notes,
entry.org_id.as_deref(),
entry.key.as_deref(),
)
})
.transpose()?;
let mut history = entry.history.clone();
Expand Down Expand Up @@ -1842,7 +1847,11 @@ pub fn edit(

let notes = notes
.map(|notes| {
crate::actions::encrypt(&notes, entry.org_id.as_deref())
crate::actions::encrypt(
&notes,
entry.org_id.as_deref(),
entry.key.as_deref(),
)
})
.transpose()?;

Expand All @@ -1860,6 +1869,7 @@ pub fn edit(
refresh_token,
&entry.id,
entry.org_id.as_deref(),
entry.key.as_deref(),
&entry.name,
&data,
&fields,
Expand Down
1 change: 1 addition & 0 deletions src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ pub enum Action {
Encrypt {
plaintext: String,
org_id: Option<String>,
entry_key: Option<String>,
},
ClipboardStore {
text: String,
Expand Down
Loading