Add the account recovery V2 upgrade routes

GET /organizations/{orgId}/users/keys/pending-v2-upgrades lists, a page at a
time, the memberships whose account recovery key still wraps the member's V1
user key after an upgrade to V2 encryption, with the upgrade token the admin
re-wraps it from. POST .../v2-upgrades stores the re-wrapped keys in one
transaction, or unenrolls the members whose token holds no usable key. Only
Owners and Admins, an Admin never for Owners, and a membership that changed
since the read is skipped. GET /organizations/{orgId}/private-key gives the
organization's private key these need, to everyone with Manage users as
upstream. The SDK's encrypted migrator calls all three.
This commit is contained in:
Daniel García committed 2026-11-14 14:24:22 +01:00
1 parent e03151d307
commit 85e87dd340
4 files changed
+305 -8

No files matched your search

+36
View File
@@ -995,6 +995,22 @@ pub async fn save_temp_file(
Ok(())
}
/// Whether `value` has the form of an RSA-encrypted string (types 3 to 6), the form of the account recovery key: the
/// type and its number of `|` pieces like the clients' `UnsignedSharedKey` parser, and each piece canonical, padded
/// base64, which is what the clients send.
///
/// Ref: <https://github.com/bitwarden/sdk-internal/blob/6238472451e4fa7b5c592b4b24a84e950a258957/crates/bitwarden-crypto/src/enc_string/asymmetric.rs#L85-L119>
pub fn is_valid_rsa_encrypted_string(value: &str) -> bool {
let (pieces, rest) = match value.split_once('.') {
Some(("3" | "4", rest)) => (1, rest),
Some(("5" | "6", rest)) => (2, rest),
_ => return false,
};
let parts: Vec<&str> = rest.split('|').collect();
parts.len() == pieces
&& parts.iter().all(|part| !part.is_empty() && data_encoding::BASE64.decode(part.as_bytes()).is_ok())
}
/// These are some tests to check that the implementations match
/// The IPv4 can be all checked in 30 seconds or so and they are correct as of nightly 2023-07-17
/// The IPV6 can't be checked in a reasonable time, so we check over a hundred billion random ones, so far correct
@@ -1073,3 +1089,23 @@ mod query_log_tests {
assert_eq!(&query[..query.floor_char_boundary(MAX_LOGGED_QUERY_BYTES)], format!("{name}="));
}
}
#[cfg(test)]
mod encrypted_string_tests {
use super::is_valid_rsa_encrypted_string as valid;
#[test]
fn rsa_encrypted_strings_are_validated() {
assert!(valid("3.YWJjZA=="));
assert!(valid("4.YWJjZA=="));
assert!(valid("6.YWJjZA==|YWJjZA=="));
assert!(!valid("2.YWJjZA==|YWJjZA==|YWJjZA=="), "symmetric type");
assert!(!valid("YWJjZA=="), "no type");
assert!(!valid("4.not base64!"));
assert!(!valid("4.YWJjZA==|YWJjZA=="), "one piece too many");
assert!(!valid("5.YWJjZA=="), "one piece too few");
assert!(!valid("4.YWJjZB=="), "padding bits set");
assert!(!valid("4."), "empty piece");
assert!(!valid(""));
}
}