-
Notifications
You must be signed in to change notification settings - Fork 4
User profiles phase 1 #410
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot reviewed 55 out of 64 changed files in this pull request and generated no comments.
Files not reviewed (9)
- backend/.sqlx/query-28b419939a08f57c86836f0fc1f8a9735debc1f68d7e46004a03328c103bc3a6.json: Language not supported
- backend/.sqlx/query-75767ac0c4fd0d80b05374023441177850ad84c37d647ae99f623f6f0438ebf6.json: Language not supported
- backend/.sqlx/query-973891122d0736c69c439a140d5cab6a2e59e00f0e52d14a214f5c48b8cbc0d8.json: Language not supported
- backend/.sqlx/query-a040c4f8d827be5ef6aa82143e9e110ae041e31f34b04ffa24a71a4254bda015.json: Language not supported
- backend/.sqlx/query-fb077ef59b81b3659e0bc280679606b0b6d620387abd433d96c7acec525f8bd6.json: Language not supported
- backend/migrations/20240927070412_create_initial_as_tables.sql: Language not supported
- backend/migrations/20240927094408_create_initial_qs_tables.sql: Language not supported
- coreclient/.sqlx/query-0c3b957fd18fd409d563d75db91f4ef129aef5222a028487218c9dcc08e146b2.json: Language not supported
- coreclient/.sqlx/query-2c5823929ace6b56806ca3e023bee1e32cf9dff0f1009f3b923a90bd402f32e4.json: Language not supported
Comments suppressed due to low confidence (2)
backend/src/ds/group_operation.rs:367
- [nitpick] Consider renaming the tuple type 'AddedUserInfo' to more explicitly reflect that it now includes an encrypted user profile key (e.g., 'AddedUserInfoWithProfile') to improve clarity for future maintainers.
type AddedUserInfo = (KeyPackage, EncryptedIdentityLinkKey, EncryptedUserProfileKey);
backend/src/ds/group_operation.rs:419
- [nitpick] Consider adding an inline comment explaining the association between the new encrypted user profile keys and the existing identity link keys to aid clarity in this multi-zip chain.
.zip(aad_payload.new_encrypted_user_profile_keys.clone())
…to konrad/as_user_profiles
boxdot
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. I added minor comments and a small comment about storage to initiate a discussion.
| friendship_token BLOB NOT NULL, | ||
| key_package_ear_key BLOB NOT NULL, | ||
| connection_key BLOB NOT NULL, | ||
| user_profile_key BLOB NOT NULL, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is not needed since contacts is recreated in the next migration.
|
|
||
| impl CoreUser { | ||
| pub async fn update_user_profile(&self, user_profile: &UserProfile) -> anyhow::Result<()> { | ||
| let mut notifier = self.store_notifier(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
notifier.notify() has to be called at some point, otherwise the notification is dropped. Here, it seems to make sense to call it after phase 1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, thanks!
| group_info, | ||
| ratchet_tree, | ||
| encrypted_identity_link_keys, | ||
| encrypted_user_profile_keys, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NB: Looks like I overlooked versioning of responses of DS
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created an issue: #415.
| pub(super) client_queue_config: QsReference, | ||
| pub(super) activity_time: TimeStamp, | ||
| pub(super) activity_epoch: GroupEpoch, | ||
| pub(super) encrypted_user_profile_key: EncryptedUserProfileKey, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will break all groups deserialization on the server, right? Asking just to make sure that we are aware of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's true.
| pub(crate) fn decrypt( | ||
| wrapper_key: &IdentityLinkWrapperKey, | ||
| encrypted_key: &EncryptedUserProfileKey, | ||
| user_name: QualifiedUserName, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, there is no need to own this value, because key derivation only needs access by ref to it.
I attached a small exercise in lifetime below. Feel free to apply it or not.
diff --git a/coreclient/src/clients/create_user.rs b/coreclient/src/clients/create_user.rs
index 0073f1f..7438fa0 100644
--- a/coreclient/src/clients/create_user.rs
+++ b/coreclient/src/clients/create_user.rs
@@ -288,7 +288,7 @@ impl PostRegistrationInitState {
qs_client_id_encryption_key: qs_encryption_key,
};
- let user_profile_key = UserProfileKey::random(user_name.clone())?;
+ let user_profile_key = UserProfileKey::random(user_name)?;
user_profile_key
.store_own(pool.acquire().await?.as_mut())
.await?;
diff --git a/coreclient/src/clients/process/process_qs.rs b/coreclient/src/clients/process/process_qs.rs
index 9e33953..b990326 100644
--- a/coreclient/src/clients/process/process_qs.rs
+++ b/coreclient/src/clients/process/process_qs.rs
@@ -315,7 +315,7 @@ impl CoreUser {
let user_profile_key = UserProfileKey::from_base_secret(
friendship_package.user_profile_base_secret.clone(),
- user_name,
+ &user_name,
)?;
// UnconfirmedConnection Phase 2: Fetch the user profile.
diff --git a/coreclient/src/contacts/mod.rs b/coreclient/src/contacts/mod.rs
index da89424..683b99e 100644
--- a/coreclient/src/contacts/mod.rs
+++ b/coreclient/src/contacts/mod.rs
@@ -58,7 +58,7 @@ impl Contact {
) -> Result<(Self, UserProfileKey), LibraryError> {
let user_profile_key = UserProfileKey::from_base_secret(
friendship_package.user_profile_base_secret,
- client_id.user_name().clone(),
+ client_id.user_name(),
)?;
let contact = Self {
user_name: client_id.user_name().clone(),
diff --git a/coreclient/src/groups/mod.rs b/coreclient/src/groups/mod.rs
index 4557adc..4bba393 100644
--- a/coreclient/src/groups/mod.rs
+++ b/coreclient/src/groups/mod.rs
@@ -412,7 +412,7 @@ impl Group {
UserProfileKey::decrypt(
welcome_attribution_info.identity_link_wrapper_key(),
&eupk,
- ci.user_name().clone(),
+ ci.user_name(),
)
.map(|user_profile_key| ProfileInfo {
user_profile_key,
@@ -530,11 +530,12 @@ impl Group {
.map(|client_auth_info| client_auth_info.client_credential().identity()),
)
.map(|(eupk, ci)| {
- UserProfileKey::decrypt(&identity_link_wrapper_key, &eupk, ci.user_name().clone())
- .map(|user_profile_key| ProfileInfo {
+ UserProfileKey::decrypt(&identity_link_wrapper_key, &eupk, ci.user_name()).map(
+ |user_profile_key| ProfileInfo {
user_profile_key,
member_id: ci.clone(),
- })
+ },
+ )
})
.collect::<Result<Vec<_>, _>>()?;
diff --git a/coreclient/src/key_stores/indexed_keys.rs b/coreclient/src/key_stores/indexed_keys.rs
index a5fba24..50c417a 100644
--- a/coreclient/src/key_stores/indexed_keys.rs
+++ b/coreclient/src/key_stores/indexed_keys.rs
@@ -27,7 +27,7 @@ use tracing::error;
// The `LABEL` constant is used to identify the key type in the database.
pub(crate) trait KeyType {
- type DerivationContext: tls_codec::Serialize + Clone;
+ type DerivationContext<'a>: tls_codec::Serialize + Clone;
const LABEL: &'static str;
}
@@ -169,16 +169,20 @@ impl<KT, ST, const LENGTH: usize> From<Secret<LENGTH>> for TypedSecret<KT, ST, L
impl<KT, ST, const LENGTH: usize> TypedSecret<KT, ST, LENGTH> {}
#[derive(TlsSerialize, TlsSize)]
-struct DerivationContext<KT: KeyType> {
- context: KT::DerivationContext,
+struct DerivationContext<'a, KT: KeyType> {
+ context: KT::DerivationContext<'a>,
key_type_instance: KeyTypeInstance<KT>,
}
-impl<KT: KeyType> KdfDerivable<BaseSecret<KT>, DerivationContext<KT>, AEAD_KEY_SIZE> for Key<KT> {
+impl<KT: KeyType> KdfDerivable<BaseSecret<KT>, DerivationContext<'_, KT>, AEAD_KEY_SIZE>
+ for Key<KT>
+{
const LABEL: &'static str = "key";
}
-impl<KT: KeyType> KdfDerivable<BaseSecret<KT>, DerivationContext<KT>, AEAD_KEY_SIZE> for Index<KT> {
+impl<KT: KeyType> KdfDerivable<BaseSecret<KT>, DerivationContext<'_, KT>, AEAD_KEY_SIZE>
+ for Index<KT>
+{
const LABEL: &'static str = "index";
}
@@ -194,7 +198,7 @@ pub(crate) struct IndexedAeadKey<KT> {
impl<KT: KeyType> IndexedAeadKey<KT> {
pub(crate) fn from_base_secret(
base_secret: BaseSecret<KT>,
- context: KT::DerivationContext,
+ context: KT::DerivationContext<'_>,
) -> Result<Self, LibraryError> {
let derive_context = DerivationContext {
context,
@@ -234,7 +238,7 @@ impl<KT> EarKey for IndexedAeadKey<KT> {}
pub(crate) struct UserProfileKeyType;
impl KeyType for UserProfileKeyType {
- type DerivationContext = QualifiedUserName;
+ type DerivationContext<'a> = &'a QualifiedUserName;
const LABEL: &'static str = "user_profile_key";
}
@@ -246,7 +250,7 @@ pub(crate) type UserProfileBaseSecret = BaseSecret<UserProfileKeyType>;
pub(crate) type UserProfileKey = IndexedAeadKey<UserProfileKeyType>;
impl UserProfileKey {
- pub(crate) fn random(user_name: QualifiedUserName) -> Result<Self, RandomnessError> {
+ pub(crate) fn random(user_name: &QualifiedUserName) -> Result<Self, RandomnessError> {
let base_secret = BaseSecret::random()?;
Self::from_base_secret(base_secret, user_name).map_err(|e| {
error!(error = %e, "Key derivation error");
@@ -264,7 +268,7 @@ impl UserProfileKey {
pub(crate) fn decrypt(
wrapper_key: &IdentityLinkWrapperKey,
encrypted_key: &EncryptedUserProfileKey,
- user_name: QualifiedUserName,
+ user_name: &QualifiedUserName,
) -> Result<Self, DecryptionError> {
let base_secret = BaseSecret::decrypt(wrapper_key, encrypted_key)?;
Self::from_base_secret(base_secret, user_name).map_err(|e| {There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I spent about half an hour trying to do that, but got tangled up in lifetime bound issues. You approach works, thanks!
| ) -> Result<(PseudonymousCredentialPlaintext, IdentityLinkKey), IdentityLinkVerificationError> | ||
| { | ||
| let identity_link_key = IdentityLinkKey::derive(connection_key, self.tbs.clone())?; | ||
| let identity_link_key = IdentityLinkKey::derive(connection_key, &self.tbs)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! (here and in other places)
| voprf_server BYTEA NOT NULL | ||
| ); | ||
|
|
||
| CREATE TYPE aead_ciphertext AS ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NB: This change will require wiping the server db.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I'll try to remember that when merging.
| ) -> Result<GetUserProfileResponse, GetUserProfileError> { | ||
| let GetUserProfileParams { client_id } = params; | ||
|
|
||
| let user_record = UserRecord::load(&self.db_pool, client_id.user_name()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a note: We could also store the encrypted user profile in S3-like storage. However, the question under which key. This could be the hashed client_id, but then only a single profile per user can be stored. Otherwise, it should be possible to use the encrypted profile key for that. Maybe we could discuss it as a possible future change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the plan is to compromise here and store larger things like the profile picture on S3 and then store on the AS DB only a struct containing things like the S3 URL, key, nonce, etc. We can do that when we also do attachments/assets.
| ) -> Result<(), StorageError> { | ||
| let password_file = BlobEncoded(&self.password_file); | ||
| query!( | ||
| "UPDATE as_user_records SET password_file = $1, encrypted_user_profile = $2 WHERE user_name = $3", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line too long.
backend/src/ds/group_operation.rs
Outdated
| let added_users = staged_commit | ||
| .add_proposals() | ||
| .map(|ap| ap.add_proposal().key_package().clone()) | ||
| .zip(aad_payload.new_encrypted_identity_link_keys.clone()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible to take aad_payload by value to avoid cloning both vectors.
raphaelrobert
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good at a conceptual level.
This PR introduces user profiles that can be encrypted and uploaded to the AS, where other users can download it when seeing the user in a group. Keys are distributed via the group state on the DS.