diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs
index 521e60c85..3020222f8 100644
--- a/crates/bitwarden-core/src/auth/auth_request.rs
+++ b/crates/bitwarden-core/src/auth/auth_request.rs
@@ -1,7 +1,7 @@
 use base64::{engine::general_purpose::STANDARD, Engine};
 use bitwarden_crypto::{
     fingerprint, generate_random_alphanumeric, AsymmetricCryptoKey, AsymmetricPublicCryptoKey,
-    CryptoError, PublicKeyEncryptionAlgorithm, UnsignedSharedKey,
+    CryptoError, PublicKeyEncryptionAlgorithm, SpkiPublicKeyBytes, UnsignedSharedKey,
 };
 #[cfg(feature = "internal")]
 use bitwarden_crypto::{EncString, SymmetricCryptoKey};
@@ -91,7 +91,9 @@ pub(crate) fn approve_auth_request(
     client: &Client,
     public_key: String,
 ) -> Result<UnsignedSharedKey, ApproveAuthRequestError> {
-    let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(public_key)?)?;
+    let public_key = AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(
+        STANDARD.decode(public_key)?,
+    ))?;
 
     let key_store = client.internal.get_key_store();
     let ctx = key_store.context();
diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs
index 570a28fd2..9b8c5db9c 100644
--- a/crates/bitwarden-core/src/auth/tde.rs
+++ b/crates/bitwarden-core/src/auth/tde.rs
@@ -1,7 +1,7 @@
 use base64::{engine::general_purpose::STANDARD, Engine};
 use bitwarden_crypto::{
-    AsymmetricPublicCryptoKey, DeviceKey, EncString, Kdf, SymmetricCryptoKey, TrustDeviceResponse,
-    UnsignedSharedKey, UserKey,
+    AsymmetricPublicCryptoKey, DeviceKey, EncString, Kdf, SpkiPublicKeyBytes, SymmetricCryptoKey,
+    TrustDeviceResponse, UnsignedSharedKey, UserKey,
 };
 
 use crate::{client::encryption_settings::EncryptionSettingsError, Client};
@@ -15,7 +15,9 @@ pub(super) fn make_register_tde_keys(
     org_public_key: String,
     remember_device: bool,
 ) -> Result<RegisterTdeKeyResponse, EncryptionSettingsError> {
-    let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(org_public_key)?)?;
+    let public_key = AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(
+        STANDARD.decode(org_public_key)?,
+    ))?;
 
     let user_key = UserKey::new(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
     let key_pair = user_key.make_key_pair()?;
diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs
index a76ea4294..97b6402dc 100644
--- a/crates/bitwarden-core/src/key_management/crypto.rs
+++ b/crates/bitwarden-core/src/key_management/crypto.rs
@@ -8,9 +8,10 @@ use std::collections::HashMap;
 
 use base64::{engine::general_purpose::STANDARD, Engine};
 use bitwarden_crypto::{
-    AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable,
-    KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, SignatureAlgorithm,
-    SignedPublicKey, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, UserKey,
+    dangerous_get_v2_rotated_account_keys, AsymmetricCryptoKey, CoseSerializable, CryptoError,
+    EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes,
+    PrimitiveEncryptable, RotatedUserKeys, SignatureAlgorithm, SignedPublicKey, SigningKey,
+    SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, UserKey,
 };
 use bitwarden_error::bitwarden_error;
 use schemars::JsonSchema;
@@ -412,7 +413,9 @@ pub(super) fn enroll_admin_password_reset(
     use base64::{engine::general_purpose::STANDARD, Engine};
     use bitwarden_crypto::AsymmetricPublicCryptoKey;
 
-    let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(public_key)?)?;
+    let public_key = AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(
+        STANDARD.decode(public_key)?,
+    ))?;
     let key_store = client.internal.get_key_store();
     let ctx = key_store.context();
     // FIXME: [PM-18110] This should be removed once the key store can handle public key encryption
@@ -613,6 +616,56 @@ pub fn make_user_signing_keys_for_enrollment(
     })
 }
 
+/// A rotated set of account keys for a user
+#[derive(Serialize, Deserialize, Debug, JsonSchema)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
+#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
+pub struct RotateUserKeysResponse {
+    /// The verifying key
+    pub verifying_key: String,
+    /// Signing key, encrypted with a symmetric key (user key, org key)
+    pub signing_key: EncString,
+    /// The user's public key, signed by the signing key
+    pub signed_public_key: String,
+    /// The user's public key, without signature
+    pub public_key: String,
+    /// The user's private key, encrypted with the user key
+    pub private_key: EncString,
+}
+
+impl From<RotatedUserKeys> for RotateUserKeysResponse {
+    fn from(rotated: RotatedUserKeys) -> Self {
+        RotateUserKeysResponse {
+            verifying_key: STANDARD.encode(rotated.verifying_key.to_vec()),
+            signing_key: rotated.signing_key,
+            signed_public_key: STANDARD.encode(rotated.signed_public_key.to_vec()),
+            public_key: STANDARD.encode(rotated.public_key.to_vec()),
+            private_key: rotated.private_key,
+        }
+    }
+}
+
+/// Gets a set of new wrapped account keys for a user, given a new user key.
+///
+/// In the current implementation, it just re-encrypts any existing keys. This function expects a
+/// user to be a v2 user; that is, they have a signing key, a cose user-key, and a private key
+pub(crate) fn get_v2_rotated_account_keys(
+    client: &Client,
+    user_key: String,
+) -> Result<RotateUserKeysResponse, CryptoError> {
+    let key_store = client.internal.get_key_store();
+    let ctx = key_store.context();
+
+    dangerous_get_v2_rotated_account_keys(
+        &SymmetricCryptoKey::try_from(user_key)?,
+        AsymmetricKeyId::UserPrivateKey,
+        SigningKeyId::UserSigningKey,
+        &ctx,
+    )
+    .map(Into::into)
+}
+
 #[cfg(test)]
 mod tests {
     use std::num::NonZeroU32;
diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs
index 03390939a..5e88f3326 100644
--- a/crates/bitwarden-core/src/key_management/crypto_client.rs
+++ b/crates/bitwarden-core/src/key_management/crypto_client.rs
@@ -18,7 +18,10 @@ use crate::key_management::crypto::{
 };
 use crate::{
     client::encryption_settings::EncryptionSettingsError,
-    key_management::crypto::CryptoClientError, Client,
+    key_management::crypto::{
+        get_v2_rotated_account_keys, CryptoClientError, RotateUserKeysResponse,
+    },
+    Client,
 };
 
 /// A client for the crypto operations.
@@ -69,6 +72,14 @@ impl CryptoClient {
     ) -> Result<MakeUserSigningKeysResponse, CryptoError> {
         make_user_signing_keys_for_enrollment(&self.client)
     }
+
+    /// Creates a rotated set of account keys for the current state
+    pub fn get_v2_rotated_account_keys(
+        &self,
+        user_key: String,
+    ) -> Result<RotateUserKeysResponse, CryptoError> {
+        get_v2_rotated_account_keys(&self.client, user_key)
+    }
 }
 
 impl CryptoClient {
diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs
index 14a400992..d65bd1bd4 100644
--- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs
+++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs
@@ -17,14 +17,14 @@ pub enum PublicKeyEncryptionAlgorithm {
     RsaOaepSha1 = 0,
 }
 
-#[derive(Clone)]
+#[derive(Clone, PartialEq)]
 pub(crate) enum RawPublicKey {
     RsaOaepSha1(RsaPublicKey),
 }
 
 /// Public key of a key pair used in a public key encryption scheme. It is used for
 /// encrypting data.
-#[derive(Clone)]
+#[derive(Clone, PartialEq)]
 pub struct AsymmetricPublicCryptoKey {
     inner: RawPublicKey,
 }
@@ -35,10 +35,11 @@ impl AsymmetricPublicCryptoKey {
     }
 
     /// Build a public key from the SubjectPublicKeyInfo DER.
-    pub fn from_der(der: &[u8]) -> Result<Self> {
+    pub fn from_der(der: &SpkiPublicKeyBytes) -> Result<Self> {
         Ok(AsymmetricPublicCryptoKey {
             inner: RawPublicKey::RsaOaepSha1(
-                RsaPublicKey::from_public_key_der(der).map_err(|_| CryptoError::InvalidKey)?,
+                RsaPublicKey::from_public_key_der(der.as_ref())
+                    .map_err(|_| CryptoError::InvalidKey)?,
             ),
         })
     }
@@ -166,8 +167,8 @@ mod tests {
 
     use crate::{
         content_format::{Bytes, Pkcs8PrivateKeyDerContentFormat},
-        AsymmetricCryptoKey, AsymmetricPublicCryptoKey, Pkcs8PrivateKeyBytes, SymmetricCryptoKey,
-        UnsignedSharedKey,
+        AsymmetricCryptoKey, AsymmetricPublicCryptoKey, Pkcs8PrivateKeyBytes, SpkiPublicKeyBytes,
+        SymmetricCryptoKey, UnsignedSharedKey,
     };
 
     #[test]
@@ -263,7 +264,8 @@ DnqOsltgPomWZ7xVfMkm9niL2OA=
 
         let private_key = Pkcs8PrivateKeyBytes::from(private_key);
         let private_key = AsymmetricCryptoKey::from_der(&private_key).unwrap();
-        let public_key = AsymmetricPublicCryptoKey::from_der(&public_key).unwrap();
+        let public_key =
+            AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(public_key)).unwrap();
 
         let raw_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
         let encrypted = UnsignedSharedKey::encapsulate_key_unsigned(&raw_key, &public_key).unwrap();
diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs
index 25407e219..5951ccf0d 100644
--- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs
+++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs
@@ -13,7 +13,7 @@ use super::AsymmetricPublicCryptoKey;
 use crate::{
     cose::CoseSerializable, error::EncodingError, util::FromStrVisitor, CoseSign1Bytes,
     CryptoError, PublicKeyEncryptionAlgorithm, RawPublicKey, SignedObject, SigningKey,
-    SigningNamespace, VerifyingKey,
+    SigningNamespace, SpkiPublicKeyBytes, VerifyingKey,
 };
 
 #[cfg(feature = "wasm")]
@@ -114,8 +114,10 @@ impl SignedPublicKey {
             public_key_message.content_format,
         ) {
             (PublicKeyEncryptionAlgorithm::RsaOaepSha1, PublicKeyFormat::Spki) => Ok(
-                AsymmetricPublicCryptoKey::from_der(&public_key_message.public_key.into_vec())
-                    .map_err(|_| EncodingError::InvalidValue("public key"))?,
+                AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(
+                    public_key_message.public_key.to_vec(),
+                ))
+                .map_err(|_| EncodingError::InvalidValue("public key"))?,
             ),
         }
     }
diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs
index 60428d8a4..d4cc15f45 100644
--- a/crates/bitwarden-crypto/src/lib.rs
+++ b/crates/bitwarden-crypto/src/lib.rs
@@ -31,7 +31,9 @@ pub use util::{generate_random_alphanumeric, generate_random_bytes, pbkdf2};
 mod wordlist;
 pub use wordlist::EFF_LONG_WORD_LIST;
 mod store;
-pub use store::{KeyStore, KeyStoreContext};
+pub use store::{
+    dangerous_get_v2_rotated_account_keys, KeyStore, KeyStoreContext, RotatedUserKeys,
+};
 mod cose;
 pub use cose::CoseSerializable;
 mod signing;
diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs
index 9675cfb5e..2df08a1f8 100644
--- a/crates/bitwarden-crypto/src/store/context.rs
+++ b/crates/bitwarden-crypto/src/store/context.rs
@@ -378,7 +378,10 @@ impl<Ids: KeyIds> KeyStoreContext<'_, Ids> {
         .ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}")))
     }
 
-    fn get_asymmetric_key(&self, key_id: Ids::Asymmetric) -> Result<&AsymmetricCryptoKey> {
+    pub(super) fn get_asymmetric_key(
+        &self,
+        key_id: Ids::Asymmetric,
+    ) -> Result<&AsymmetricCryptoKey> {
         if key_id.is_local() {
             self.local_asymmetric_keys.get(key_id)
         } else {
@@ -387,7 +390,7 @@ impl<Ids: KeyIds> KeyStoreContext<'_, Ids> {
         .ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}")))
     }
 
-    fn get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> {
+    pub(super) fn get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> {
         if key_id.is_local() {
             self.local_signing_keys.get(key_id)
         } else {
diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs
new file mode 100644
index 000000000..a74edb2ec
--- /dev/null
+++ b/crates/bitwarden-crypto/src/store/key_rotation.rs
@@ -0,0 +1,140 @@
+use crate::{
+    CoseKeyBytes, CoseSerializable, CoseSign1Bytes, CryptoError, EncString, KeyEncryptable, KeyIds,
+    KeyStoreContext, SignedPublicKeyMessage, SpkiPublicKeyBytes, SymmetricCryptoKey,
+};
+
+/// Rotated set of account keys
+pub struct RotatedUserKeys {
+    /// The verifying key
+    pub verifying_key: CoseKeyBytes,
+    /// Signing key, encrypted with a symmetric key (user key, org key)
+    pub signing_key: EncString,
+    /// The user's public key, signed by the signing key
+    pub signed_public_key: CoseSign1Bytes,
+    /// The user's public key, without signature
+    pub public_key: SpkiPublicKeyBytes,
+    /// The user's private key, encrypted with the user key
+    pub private_key: EncString,
+}
+
+/// Re-encrypts the user's keys with the provided symmetric key for a v2 user.
+pub fn dangerous_get_v2_rotated_account_keys<Ids: KeyIds>(
+    new_user_key: &SymmetricCryptoKey,
+    current_user_private_key_id: Ids::Asymmetric,
+    current_user_signing_key_id: Ids::Signing,
+    ctx: &KeyStoreContext<Ids>,
+) -> Result<RotatedUserKeys, CryptoError> {
+    let current_private_key = ctx.get_asymmetric_key(current_user_private_key_id)?;
+    let current_signing_key = ctx.get_signing_key(current_user_signing_key_id)?;
+
+    let signed_public_key =
+        SignedPublicKeyMessage::from_public_key(&current_private_key.to_public_key())?
+            .sign(current_signing_key)?;
+    Ok(RotatedUserKeys {
+        verifying_key: current_signing_key.to_verifying_key().to_cose(),
+        signing_key: current_signing_key
+            .to_cose()
+            .encrypt_with_key(new_user_key)?,
+        signed_public_key: signed_public_key.into(),
+        public_key: current_private_key.to_public_key().to_der()?,
+        private_key: current_private_key
+            .to_der()?
+            .encrypt_with_key(new_user_key)?,
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        traits::tests::{TestAsymmKey, TestIds, TestSigningKey, TestSymmKey},
+        AsymmetricCryptoKey, KeyDecryptable, KeyStore, Pkcs8PrivateKeyBytes,
+        PublicKeyEncryptionAlgorithm, SignedPublicKey, SigningKey,
+    };
+
+    #[test]
+    fn test_account_key_rotation() {
+        let store: KeyStore<TestIds> = KeyStore::default();
+        let mut ctx = store.context_mut();
+
+        // Generate a new user key
+        let new_user_key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
+        let current_user_private_key_id = TestAsymmKey::A(0);
+        let current_user_signing_key_id = TestSigningKey::A(0);
+
+        // Make the keys
+        ctx.generate_symmetric_key(TestSymmKey::A(0)).unwrap();
+        ctx.make_signing_key(current_user_signing_key_id).unwrap();
+        #[allow(deprecated)]
+        ctx.set_asymmetric_key(
+            current_user_private_key_id,
+            AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1),
+        )
+        .unwrap();
+
+        // Get the rotated account keys
+        let rotated_keys = dangerous_get_v2_rotated_account_keys(
+            &new_user_key,
+            current_user_private_key_id,
+            current_user_signing_key_id,
+            &ctx,
+        )
+        .unwrap();
+
+        // Public/Private key
+        assert_eq!(
+            rotated_keys.public_key,
+            ctx.get_asymmetric_key(current_user_private_key_id)
+                .unwrap()
+                .to_public_key()
+                .to_der()
+                .unwrap()
+        );
+        let decrypted_private_key: Vec<u8> = rotated_keys
+            .private_key
+            .decrypt_with_key(&new_user_key)
+            .unwrap();
+        let private_key =
+            AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(decrypted_private_key))
+                .unwrap();
+        assert_eq!(
+            private_key.to_der().unwrap(),
+            ctx.get_asymmetric_key(current_user_private_key_id)
+                .unwrap()
+                .to_der()
+                .unwrap()
+        );
+
+        // Signing Key
+        let decrypted_signing_key: Vec<u8> = rotated_keys
+            .signing_key
+            .decrypt_with_key(&new_user_key)
+            .unwrap();
+        let signing_key =
+            SigningKey::from_cose(&CoseKeyBytes::from(decrypted_signing_key)).unwrap();
+        assert_eq!(
+            signing_key.to_cose(),
+            ctx.get_signing_key(current_user_signing_key_id)
+                .unwrap()
+                .to_cose(),
+        );
+
+        // Signed Public Key
+        let signed_public_key = SignedPublicKey::try_from(rotated_keys.signed_public_key).unwrap();
+        let unwrapped_key = signed_public_key
+            .verify_and_unwrap(
+                &ctx.get_signing_key(current_user_signing_key_id)
+                    .unwrap()
+                    .to_verifying_key(),
+            )
+            .unwrap();
+        assert_eq!(
+            unwrapped_key.to_der().unwrap(),
+            ctx.get_asymmetric_key(current_user_private_key_id)
+                .unwrap()
+                .to_public_key()
+                .to_der()
+                .unwrap()
+        );
+    }
+}
diff --git a/crates/bitwarden-crypto/src/store/mod.rs b/crates/bitwarden-crypto/src/store/mod.rs
index 765762bac..c1425bf30 100644
--- a/crates/bitwarden-crypto/src/store/mod.rs
+++ b/crates/bitwarden-crypto/src/store/mod.rs
@@ -35,6 +35,9 @@ use backend::{create_store, StoreBackend};
 use context::GlobalKeys;
 pub use context::KeyStoreContext;
 
+mod key_rotation;
+pub use key_rotation::*;
+
 /// An in-memory key store that provides a safe and secure way to store keys and use them for
 /// encryption/decryption operations. The store API is designed to work only on key identifiers
 /// ([KeyId]). These identifiers are user-defined types that contain no key material, which means
diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs
index 13ab4da05..20f8588a7 100644
--- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs
+++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs
@@ -251,7 +251,9 @@ impl PureCrypto {
         shared_key: Vec<u8>,
         encapsulation_key: Vec<u8>,
     ) -> Result<String, CryptoError> {
-        let encapsulation_key = AsymmetricPublicCryptoKey::from_der(encapsulation_key.as_slice())?;
+        let encapsulation_key = AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(
+            encapsulation_key.as_slice(),
+        ))?;
         Ok(UnsignedSharedKey::encapsulate_key_unsigned(
             &SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(shared_key))?,
             &encapsulation_key,