diff --git a/app/build.gradle b/app/build.gradle index 5a587e327..00f158d33 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -193,13 +193,13 @@ android { buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips buildConfigField "String", "SIGNAL_AGENT", "\"OWA\"" buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\"" - buildConfigField "String", "CDSI_MRENCLAVE", "\"42e36b74794abe612d698308b148ff8a7dc5fdc6ad28d99bc5024ed6ece18dfe\"" + buildConfigField "String", "CDSI_MRENCLAVE", "\"e5eaa62da3514e8b37ccabddb87e52e7f319ccf5120a13f9e1b42b87ec9dd3dd\"" buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " + "\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " + "\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")" buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"" - buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXQ==\"" + buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P\"" buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}' buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode" buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\"" @@ -337,7 +337,7 @@ android { "\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")" buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"" - buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXQ==\"" + buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj\"" buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\"" buildConfigField "String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\"" buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\"" diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt index 553a6274d..ceab44e4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt @@ -77,6 +77,9 @@ object ContactDiscoveryRefreshV2 { ) stopwatch.split("recipient-db") + SignalDatabase.recipients.bulkUpdatedRegisteredStatus(registeredIds.associateWith { null }, emptyList()) + stopwatch.split("update-registered") + stopwatch.stop(TAG) return ContactDiscovery.RefreshResult(registeredIds, emptyMap()) @@ -127,6 +130,9 @@ object ContactDiscoveryRefreshV2 { ) stopwatch.split("recipient-db") + SignalDatabase.recipients.bulkUpdatedRegisteredStatus(registeredIds.associateWith { null }, emptyList()) + stopwatch.split("update-registered") + stopwatch.stop(TAG) return ContactDiscovery.RefreshResult(registeredIds, emptyMap()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java index 23ae48498..a2c325d41 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java @@ -311,6 +311,7 @@ import org.thoughtcrime.securesms.verify.VerifyIdentityActivity; import org.thoughtcrime.securesms.wallpaper.ChatWallpaper; import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil; import org.whispersystems.signalservice.api.SignalSessionLock; +import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil; import java.io.IOException; import java.security.SecureRandom; @@ -1267,7 +1268,7 @@ public class ConversationParentFragment extends Fragment AttachmentManager.selectLocation(this, PICK_LOCATION, getSendButtonColor(sendButton.getSelectedSendType())); break; case PAYMENT: - if (recipient.get().hasProfileKeyCredential()) { + if (ExpiringProfileCredentialUtil.isValid(recipient.get().getExpiringProfileKeyCredential())) { AttachmentManager.selectPayment(this, recipient.getId()); } else { CanNotSendPaymentDialog.show(requireActivity()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java index a740f2cc9..9b742d56a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java @@ -6,7 +6,6 @@ import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.profiles.ProfileKey; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Util; @@ -63,18 +62,6 @@ public final class ProfileKeyUtil { } } - public static @Nullable ProfileKeyCredential profileKeyCredentialOrNull(@Nullable byte[] profileKeyCredential) { - if (profileKeyCredential != null) { - try { - return new ProfileKeyCredential(profileKeyCredential); - } catch (InvalidInputException e) { - Log.w(TAG, String.format(Locale.US, "Seen non-null profile key credential of wrong length %d", profileKeyCredential.length), e); - } - } - - return null; - } - public static @NonNull ProfileKey profileKeyOrThrow(@NonNull byte[] profileKey) { try { return new ProfileKey(profileKey); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 0c5a364ce..2cf4bc31a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -83,6 +83,9 @@ public class GroupDatabase extends Database { private static final String UNMIGRATED_V1_MEMBERS = "former_v1_members"; private static final String DISTRIBUTION_ID = "distribution_id"; private static final String DISPLAY_AS_STORY = "display_as_story"; + + /** Was temporarily used for PNP accept by pni but is no longer needed/updated */ + @Deprecated private static final String AUTH_SERVICE_ID = "auth_service_id"; @@ -125,7 +128,7 @@ public class GroupDatabase extends Database { private static final String[] GROUP_PROJECTION = { GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, UNMIGRATED_V1_MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST, - TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP, AUTH_SERVICE_ID + TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP }; static final List TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList(); @@ -477,25 +480,23 @@ public class GroupDatabase extends Database { if (groupExists(groupId.deriveV2MigrationGroupId())) { throw new LegacyGroupInsertException(groupId); } - create(null, groupId, title, members, avatar, relay, null, null); + create(groupId, title, members, avatar, relay, null, null); } public void create(@NonNull GroupId.Mms groupId, @Nullable String title, @NonNull Collection members) { - create(null, groupId, Util.isEmpty(title) ? null : title, members, null, null, null, null); + create(groupId, Util.isEmpty(title) ? null : title, members, null, null, null, null); } - public GroupId.V2 create(@Nullable ServiceId authServiceId, - @NonNull GroupMasterKey groupMasterKey, + public GroupId.V2 create(@NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup groupState) { - return create(authServiceId, groupMasterKey, groupState, false); + return create(groupMasterKey, groupState, false); } - public GroupId.V2 create(@Nullable ServiceId authServiceId, - @NonNull GroupMasterKey groupMasterKey, + public GroupId.V2 create(@NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup groupState, boolean force) { @@ -507,7 +508,7 @@ public class GroupDatabase extends Database { Log.w(TAG, "Forcing the creation of a group even though we already have a V1 ID!"); } - create(authServiceId, groupId, groupState.getTitle(), Collections.emptyList(), null, null, groupMasterKey, groupState); + create(groupId, groupState.getTitle(), Collections.emptyList(), null, null, groupMasterKey, groupState); return groupId; } @@ -537,8 +538,8 @@ public class GroupDatabase extends Database { if (updated < 1) { Log.w(TAG, "No group entry. Creating restore placeholder for " + groupId); - create(authServiceId, - groupMasterKey, + create( + groupMasterKey, DecryptedGroup.newBuilder() .setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) .build(), @@ -559,8 +560,7 @@ public class GroupDatabase extends Database { /** * @param groupMasterKey null for V1, must be non-null for V2 (presence dictates group version). */ - private void create(@Nullable ServiceId authServiceId, - @NonNull GroupId groupId, + private void create(@NonNull GroupId groupId, @Nullable String title, @NonNull Collection memberCollection, @Nullable SignalServiceAttachmentPointer avatar, @@ -575,7 +575,6 @@ public class GroupDatabase extends Database { Collections.sort(members); ContentValues contentValues = new ContentValues(); - contentValues.put(AUTH_SERVICE_ID, authServiceId != null ? authServiceId.toString() : null); contentValues.put(RECIPIENT_ID, groupRecipientId.serialize()); contentValues.put(GROUP_ID, groupId.toString()); contentValues.put(TITLE, title); @@ -987,13 +986,6 @@ public class GroupDatabase extends Database { return result; } - public void setAuthServiceId(@Nullable ServiceId authServiceId, @NonNull GroupId groupId) { - ContentValues values = new ContentValues(1); - values.put(AUTH_SERVICE_ID, authServiceId == null ? null : authServiceId.toString()); - - getWritableDatabase().update(TABLE_NAME, values, GROUP_ID + " = ?", SqlUtil.buildArgs(groupId)); - } - public static class Reader implements Closeable { public final Cursor cursor; @@ -1038,8 +1030,7 @@ public class GroupDatabase extends Database { CursorUtil.requireBlob(cursor, V2_MASTER_KEY), CursorUtil.requireInt(cursor, V2_REVISION), CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP), - CursorUtil.getString(cursor, DISTRIBUTION_ID).map(DistributionId::from).orElse(null), - CursorUtil.requireString(cursor, AUTH_SERVICE_ID)); + CursorUtil.getString(cursor, DISTRIBUTION_ID).map(DistributionId::from).orElse(null)); } @Override @@ -1065,7 +1056,6 @@ public class GroupDatabase extends Database { private final boolean mms; @Nullable private final V2GroupProperties v2GroupProperties; private final DistributionId distributionId; - @Nullable private final String authServiceId; public GroupRecord(@NonNull GroupId id, @NonNull RecipientId recipientId, @@ -1082,8 +1072,7 @@ public class GroupDatabase extends Database { @Nullable byte[] groupMasterKeyBytes, int groupRevision, @Nullable byte[] decryptedGroupBytes, - @Nullable DistributionId distributionId, - @Nullable String authServiceId) + @Nullable DistributionId distributionId) { this.id = id; this.recipientId = recipientId; @@ -1096,7 +1085,6 @@ public class GroupDatabase extends Database { this.active = active; this.mms = mms; this.distributionId = distributionId; - this.authServiceId = authServiceId; V2GroupProperties v2GroupProperties = null; if (groupMasterKeyBytes != null && decryptedGroupBytes != null) { @@ -1285,10 +1273,6 @@ public class GroupDatabase extends Database { } return false; } - - public @Nullable ServiceId getAuthServiceId() { - return ServiceId.parseOrNull(authServiceId); - } } public static class V2GroupProperties { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt index 76616c978..05b7dbad1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -33,8 +33,8 @@ import org.signal.core.util.withinTransaction import org.signal.libsignal.protocol.IdentityKey import org.signal.libsignal.protocol.InvalidKeyException import org.signal.libsignal.zkgroup.InvalidInputException +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential import org.signal.libsignal.zkgroup.profiles.ProfileKey -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential import org.signal.storageservice.protos.groups.local.DecryptedGroup import org.thoughtcrime.securesms.badges.Badges import org.thoughtcrime.securesms.badges.Badges.toDatabaseBadge @@ -66,7 +66,7 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime -import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileKeyCredentialColumnData +import org.thoughtcrime.securesms.database.model.databaseprotos.ExpiringProfileKeyCredentialColumnData import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper import org.thoughtcrime.securesms.dependencies.ApplicationDependencies @@ -154,7 +154,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : private const val SYSTEM_CONTACT_URI = "system_contact_uri" private const val SYSTEM_INFO_PENDING = "system_info_pending" private const val PROFILE_KEY = "profile_key" - private const val PROFILE_KEY_CREDENTIAL = "profile_key_credential" + const val EXPIRING_PROFILE_KEY_CREDENTIAL = "profile_key_credential" private const val SIGNAL_PROFILE_AVATAR = "signal_profile_avatar" const val PROFILE_SHARING = "profile_sharing" private const val LAST_PROFILE_FETCH = "last_profile_fetch" @@ -214,7 +214,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : $SYSTEM_CONTACT_URI TEXT DEFAULT NULL, $SYSTEM_INFO_PENDING INTEGER DEFAULT 0, $PROFILE_KEY TEXT DEFAULT NULL, - $PROFILE_KEY_CREDENTIAL TEXT DEFAULT NULL, + $EXPIRING_PROFILE_KEY_CREDENTIAL TEXT DEFAULT NULL, $PROFILE_GIVEN_NAME TEXT DEFAULT NULL, $PROFILE_FAMILY_NAME TEXT DEFAULT NULL, $PROFILE_JOINED_NAME TEXT DEFAULT NULL, @@ -269,7 +269,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : MESSAGE_EXPIRATION_TIME, REGISTERED, PROFILE_KEY, - PROFILE_KEY_CREDENTIAL, + EXPIRING_PROFILE_KEY_CREDENTIAL, SYSTEM_JOINED_NAME, SYSTEM_GIVEN_NAME, SYSTEM_FAMILY_NAME, @@ -925,7 +925,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : val recipientId = getByStorageKeyOrThrow(update.new.id.raw) if (StorageSyncHelper.profileKeyChanged(update)) { val clearValues = ContentValues(1).apply { - putNull(PROFILE_KEY_CREDENTIAL) + putNull(EXPIRING_PROFILE_KEY_CREDENTIAL) } db.update(TABLE_NAME, clearValues, ID_WHERE, SqlUtil.buildArgs(recipientId)) } @@ -986,7 +986,6 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : Log.i(TAG, "Creating restore placeholder for $groupId") groups.create( - null, masterKey, DecryptedGroup.newBuilder() .setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) @@ -1560,7 +1559,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } val valuesToSet = ContentValues(3).apply { put(PROFILE_KEY, encodedProfileKey) - putNull(PROFILE_KEY_CREDENTIAL) + putNull(EXPIRING_PROFILE_KEY_CREDENTIAL) put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.mode) } @@ -1592,7 +1591,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : val args = arrayOf(id.serialize()) val valuesToSet = ContentValues(3).apply { put(PROFILE_KEY, Base64.encodeBytes(profileKey.serialize())) - putNull(PROFILE_KEY_CREDENTIAL) + putNull(EXPIRING_PROFILE_KEY_CREDENTIAL) put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.mode) } @@ -1611,16 +1610,16 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : fun setProfileKeyCredential( id: RecipientId, profileKey: ProfileKey, - profileKeyCredential: ProfileKeyCredential + expiringProfileKeyCredential: ExpiringProfileKeyCredential ): Boolean { val selection = "$ID = ? AND $PROFILE_KEY = ?" val args = arrayOf(id.serialize(), Base64.encodeBytes(profileKey.serialize())) - val columnData = ProfileKeyCredentialColumnData.newBuilder() + val columnData = ExpiringProfileKeyCredentialColumnData.newBuilder() .setProfileKey(ByteString.copyFrom(profileKey.serialize())) - .setProfileKeyCredential(ByteString.copyFrom(profileKeyCredential.serialize())) + .setExpiringProfileKeyCredential(ByteString.copyFrom(expiringProfileKeyCredential.serialize())) .build() val values = ContentValues(1).apply { - put(PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(columnData.toByteArray())) + put(EXPIRING_PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(columnData.toByteArray())) } val updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values) @@ -1634,11 +1633,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : fun clearProfileKeyCredential(id: RecipientId) { val values = ContentValues(1) - values.putNull(PROFILE_KEY_CREDENTIAL) - values.putNull(PROFILE_KEY) - values.put(PROFILE_SHARING, 0) + values.putNull(EXPIRING_PROFILE_KEY_CREDENTIAL) if (update(id, values)) { - rotateStorageId(id) ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id) } } @@ -2101,11 +2097,11 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : db.beginTransaction() try { - for ((recipientId, aci) in registered) { + for ((recipientId, serviceId) in registered) { val values = ContentValues(2).apply { put(REGISTERED, RegisteredState.REGISTERED.id) - if (aci != null) { - put(SERVICE_ID, aci.toString().lowercase()) + if (serviceId != null) { + put(SERVICE_ID, serviceId.toString().lowercase()) } } @@ -2117,7 +2113,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } catch (e: SQLiteConstraintException) { Log.w(TAG, "[bulkUpdateRegisteredStatus] Hit a conflict when trying to update $recipientId. Possibly merging.") val e164 = getRecord(recipientId).e164 - val newId = getAndPossiblyMerge(aci, e164) + val newId = getAndPossiblyMerge(serviceId, e164) Log.w(TAG, "[bulkUpdateRegisteredStatus] Merged into $newId") } } @@ -2173,7 +2169,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } /** - * Processes CDSv2 results, merging recipients as necessary. + * Processes CDSv2 results, merging recipients as necessary. Does not mark users as + * registered. * * Important: This is under active development and is not suitable for actual use. * @@ -3489,6 +3486,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } } .run() + + ApplicationDependencies.getRecipientCache().clear() + RecipientId.clearCache() } /** @@ -3499,7 +3499,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : .update(TABLE_NAME) .values( PROFILE_KEY to null, - PROFILE_KEY_CREDENTIAL to null, + EXPIRING_PROFILE_KEY_CREDENTIAL to null, PROFILE_GIVEN_NAME to null, PROFILE_FAMILY_NAME to null, PROFILE_JOINED_NAME to null, @@ -3514,6 +3514,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } } .run() + + ApplicationDependencies.getRecipientCache().clear() + RecipientId.clearCache() } fun getRecord(context: Context, cursor: Cursor): RecipientRecord { @@ -3522,9 +3525,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : fun getRecord(context: Context, cursor: Cursor, idColumnName: String): RecipientRecord { val profileKeyString = cursor.requireString(PROFILE_KEY) - val profileKeyCredentialString = cursor.requireString(PROFILE_KEY_CREDENTIAL) + val expiringProfileKeyCredentialString = cursor.requireString(EXPIRING_PROFILE_KEY_CREDENTIAL) var profileKey: ByteArray? = null - var profileKeyCredential: ProfileKeyCredential? = null + var expiringProfileKeyCredential: ExpiringProfileKeyCredential? = null if (profileKeyString != null) { try { @@ -3533,12 +3536,12 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : Log.w(TAG, e) } - if (profileKeyCredentialString != null) { + if (expiringProfileKeyCredentialString != null) { try { - val columnDataBytes = Base64.decode(profileKeyCredentialString) - val columnData = ProfileKeyCredentialColumnData.parseFrom(columnDataBytes) + val columnDataBytes = Base64.decode(expiringProfileKeyCredentialString) + val columnData = ExpiringProfileKeyCredentialColumnData.parseFrom(columnDataBytes) if (Arrays.equals(columnData.profileKey.toByteArray(), profileKey)) { - profileKeyCredential = ProfileKeyCredential(columnData.profileKeyCredential.toByteArray()) + expiringProfileKeyCredential = ExpiringProfileKeyCredential(columnData.expiringProfileKeyCredential.toByteArray()) } else { Log.i(TAG, "Out of date profile key credential data ignored on read") } @@ -3598,7 +3601,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : expireMessages = cursor.requireInt(MESSAGE_EXPIRATION_TIME), registered = RegisteredState.fromId(cursor.requireInt(REGISTERED)), profileKey = profileKey, - profileKeyCredential = profileKeyCredential, + expiringProfileKeyCredential = expiringProfileKeyCredential, systemProfileName = ProfileName.fromParts(cursor.requireString(SYSTEM_GIVEN_NAME), cursor.requireString(SYSTEM_FAMILY_NAME)), systemDisplayName = cursor.requireString(SYSTEM_JOINED_NAME), systemContactPhotoUri = cursor.requireString(SYSTEM_PHOTO_URI), @@ -3688,7 +3691,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : private fun updateProfileValuesForMerge(values: ContentValues, record: RecipientRecord) { values.apply { put(PROFILE_KEY, if (record.profileKey != null) Base64.encodeBytes(record.profileKey) else null) - putNull(PROFILE_KEY_CREDENTIAL) + putNull(EXPIRING_PROFILE_KEY_CREDENTIAL) put(SIGNAL_PROFILE_AVATAR, record.signalProfileAvatar) put(PROFILE_GIVEN_NAME, record.signalProfileName.givenName) put(PROFILE_FAMILY_NAME, record.signalProfileName.familyName) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 6a6c2d2b6..78cf5b7af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -202,8 +202,9 @@ object SignalDatabaseMigrations { private const val REMOTE_MEGAPHONE = 146 private const val QUOTE_INDEX = 147 private const val MY_STORY_PRIVACY_MODE = 148 + private const val EXPIRING_PROFILE_CREDENTIALS = 149 - const val DATABASE_VERSION = 148 + const val DATABASE_VERSION = 149 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -2658,6 +2659,10 @@ object SignalDatabaseMigrations { db.execSQL("CREATE UNIQUE INDEX distribution_list_member_list_id_recipient_id_privacy_mode_index ON distribution_list_member (list_id, recipient_id, privacy_mode)") } + + if (oldVersion < EXPIRING_PROFILE_CREDENTIALS) { + db.execSQL("UPDATE recipient SET profile_key_credential = NULL") + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java index 2c54ed79e..9ac330c9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java @@ -28,13 +28,13 @@ import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember; import org.signal.storageservice.protos.groups.local.EnabledState; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil; -import org.thoughtcrime.securesms.keyvalue.ServiceIds; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.ExpirationUtil; import org.thoughtcrime.securesms.util.SpanUtil; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.push.ServiceId; +import org.whispersystems.signalservice.api.push.ServiceIds; import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.Arrays; @@ -91,8 +91,8 @@ final class GroupsV2UpdateMessageProducer { } if (DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfIds.getAci().uuid()).isPresent() || - (selfIds.getPni() != null && DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfIds.getPni().uuid()).isPresent()) - ) { + (selfIds.getPni() != null && DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfIds.getPni().uuid()).isPresent())) + { return updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16); } else { return updateDescription(context.getString(R.string.MessageRecord_group_updated), R.drawable.ic_update_group_16); @@ -124,6 +124,7 @@ final class GroupsV2UpdateMessageProducer { describeUnknownEditorRequestingMembersApprovals(change, updates); describeUnknownEditorRequestingMembersDeletes(change, updates); describeUnknownEditorAnnouncementGroupChange(change, updates); + describeUnknownEditorPromotePendingPniAci(change, updates); describeUnknownEditorMemberRemovals(change, updates); @@ -149,6 +150,7 @@ final class GroupsV2UpdateMessageProducer { describeRequestingMembersApprovals(change, updates); describeRequestingMembersDeletes(change, updates); describeAnnouncementGroupChange(change, updates); + describePromotePendingPniAci(change, updates); describeMemberRemovals(change, updates); @@ -304,8 +306,8 @@ final class GroupsV2UpdateMessageProducer { } private void describeInvitations(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = selfIds.matches(change.getEditor()); - int notYouInviteCount = 0; + boolean editorIsYou = selfIds.matches(change.getEditor()); + int notYouInviteCount = 0; for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) { boolean newMemberIsYou = selfIds.matches(invitee.getUuid()); @@ -351,8 +353,8 @@ final class GroupsV2UpdateMessageProducer { } private void describeRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = selfIds.matches(change.getEditor()); - int notDeclineCount = 0; + boolean editorIsYou = selfIds.matches(change.getEditor()); + int notDeclineCount = 0; for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) { boolean decline = invitee.getUuid().equals(change.getEditor()); @@ -400,8 +402,8 @@ final class GroupsV2UpdateMessageProducer { boolean editorIsYou = selfIds.matches(change.getEditor()); for (DecryptedMember newMember : change.getPromotePendingMembersList()) { - ByteString uuid = newMember.getUuid(); - boolean newMemberIsYou = selfIds.matches(uuid); + ByteString uuid = newMember.getUuid(); + boolean newMemberIsYou = selfIds.matches(uuid); if (editorIsYou) { if (newMemberIsYou) { @@ -425,8 +427,8 @@ final class GroupsV2UpdateMessageProducer { private void describeUnknownEditorPromotePending(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (DecryptedMember newMember : change.getPromotePendingMembersList()) { - ByteString uuid = newMember.getUuid(); - boolean newMemberIsYou = selfIds.matches(uuid); + ByteString uuid = newMember.getUuid(); + boolean newMemberIsYou = selfIds.matches(uuid); if (newMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16)); @@ -679,7 +681,7 @@ final class GroupsV2UpdateMessageProducer { if (requestingMemberIsYou) { updates.add(updateDescription(R.string.MessageRecord_s_approved_your_request_to_join_the_group, change.getEditor(), R.drawable.ic_update_group_accept_16)); } else { - boolean editorIsYou = selfIds.matches(change.getEditor()); + boolean editorIsYou = selfIds.matches(change.getEditor()); if (editorIsYou) { updates.add(updateDescription(R.string.MessageRecord_you_approved_a_request_to_join_the_group_from_s, requestingMember.getUuid(), R.drawable.ic_update_group_accept_16)); @@ -770,6 +772,46 @@ final class GroupsV2UpdateMessageProducer { } } + private void describePromotePendingPniAci(@NonNull DecryptedGroupChange change, @NonNull List updates) { + boolean editorIsYou = selfIds.matches(change.getEditor()); + + for (DecryptedMember newMember : change.getPromotePendingPniAciMembersList()) { + ByteString uuid = newMember.getUuid(); + boolean newMemberIsYou = selfIds.matches(uuid); + + if (editorIsYou) { + if (newMemberIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_accepted_invite), R.drawable.ic_update_group_accept_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_you_added_invited_member_s, uuid, R.drawable.ic_update_group_add_16)); + } + } else { + if (newMemberIsYou) { + updates.add(updateDescription(R.string.MessageRecord_s_added_you, change.getEditor(), R.drawable.ic_update_group_add_16)); + } else { + if (uuid.equals(change.getEditor())) { + updates.add(updateDescription(R.string.MessageRecord_s_accepted_invite, uuid, R.drawable.ic_update_group_accept_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_added_invited_member_s, change.getEditor(), uuid, R.drawable.ic_update_group_add_16)); + } + } + } + } + } + + private void describeUnknownEditorPromotePendingPniAci(@NonNull DecryptedGroupChange change, @NonNull List updates) { + for (DecryptedMember newMember : change.getPromotePendingPniAciMembersList()) { + ByteString uuid = newMember.getUuid(); + boolean newMemberIsYou = selfIds.matches(uuid); + + if (newMemberIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_joined_the_group, uuid, R.drawable.ic_update_group_add_16)); + } + } + } + private static UpdateDescription updateDescription(@NonNull String string, @DrawableRes int iconResource) { return UpdateDescription.staticDescription(string, iconResource); } @@ -868,8 +910,8 @@ final class GroupsV2UpdateMessageProducer { @VisibleForTesting static @NonNull Spannable makeRecipientsClickable(@NonNull Context context, @NonNull String template, @NonNull List recipientIds, @Nullable Consumer clickHandler) { - SpannableStringBuilder builder = new SpannableStringBuilder(); - int startIndex = 0; + SpannableStringBuilder builder = new SpannableStringBuilder(); + int startIndex = 0; Map idByPlaceholder = new HashMap<>(); for (RecipientId id : recipientIds) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt index 19f329a22..37974227a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt @@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database.model import android.net.Uri import org.signal.libsignal.zkgroup.groups.GroupMasterKey -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.conversation.colors.ChatColors @@ -45,7 +45,7 @@ data class RecipientRecord( val expireMessages: Int, val registered: RegisteredState, val profileKey: ByteArray?, - val profileKeyCredential: ProfileKeyCredential?, + val expiringProfileKeyCredential: ExpiringProfileKeyCredential?, val systemProfileName: ProfileName, val systemDisplayName: String?, val systemContactPhotoUri: String?, diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index cf888f749..548d93b33 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -169,10 +169,9 @@ public class ApplicationDependencies { if (groupsV2Authorization == null) { synchronized (LOCK) { if (groupsV2Authorization == null) { - GroupsV2Authorization.ValueCache aciAuthCache = new GroupsV2AuthorizationMemoryValueCache(SignalStore.groupsV2AciAuthorizationCache()); - GroupsV2Authorization.ValueCache pniAuthCache = new GroupsV2AuthorizationMemoryValueCache(SignalStore.groupsV2PniAuthorizationCache()); + GroupsV2Authorization.ValueCache authCache = new GroupsV2AuthorizationMemoryValueCache(SignalStore.groupsV2AciAuthorizationCache()); - groupsV2Authorization = new GroupsV2Authorization(getSignalServiceAccountManager().getGroupsV2Api(), aciAuthCache, pniAuthCache); + groupsV2Authorization = new GroupsV2Authorization(getSignalServiceAccountManager().getGroupsV2Api(), authCache); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index 88dea3d00..07f372c44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -177,7 +177,6 @@ public final class GroupManager { */ @WorkerThread public static void updateGroupFromServer(@NonNull Context context, - @NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey, int revision, long timestamp, @@ -185,18 +184,18 @@ public final class GroupManager { throws GroupChangeBusyException, IOException, GroupNotAMemberException { try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) { - updater.updateLocalToServerRevision(authServiceId, revision, timestamp, signedGroupChange); + updater.updateLocalToServerRevision(revision, timestamp, signedGroupChange); } } @WorkerThread public static V2GroupServerStatus v2GroupStatus(@NonNull Context context, - @NonNull ServiceId authServiceserviceId, + @NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey) throws IOException { try { - new GroupManagerV2(context).groupServerQuery(authServiceserviceId, groupMasterKey); + new GroupManagerV2(context).groupServerQuery(authServiceId, groupMasterKey); return V2GroupServerStatus.FULL_OR_PENDING_MEMBER; } catch (GroupNotAMemberException e) { return V2GroupServerStatus.NOT_A_MEMBER; diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index 1e2e6863c..61d7e06e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -19,8 +19,8 @@ import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher; import org.signal.libsignal.zkgroup.groups.GroupMasterKey; import org.signal.libsignal.zkgroup.groups.GroupSecretParams; import org.signal.libsignal.zkgroup.groups.UuidCiphertext; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.GroupExternalCredential; @@ -51,6 +51,8 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.sms.MessageSender; +import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.ProfileUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.groupsv2.GroupCandidate; @@ -64,6 +66,7 @@ import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2Change import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.ServiceId; +import org.whispersystems.signalservice.api.push.ServiceIds; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.ConflictException; import org.whispersystems.signalservice.api.util.UuidUtil; @@ -97,6 +100,7 @@ final class GroupManagerV2 { private final GroupsV2Operations groupsV2Operations; private final GroupsV2Authorization authorization; private final GroupsV2StateProcessor groupsV2StateProcessor; + private final ServiceIds serviceIds; private final ACI selfAci; private final PNI selfPni; private final GroupCandidateHelper groupCandidateHelper; @@ -109,9 +113,8 @@ final class GroupManagerV2 { ApplicationDependencies.getGroupsV2Operations(), ApplicationDependencies.getGroupsV2Authorization(), ApplicationDependencies.getGroupsV2StateProcessor(), - SignalStore.account().requireAci(), - SignalStore.account().requirePni(), - new GroupCandidateHelper(context), + SignalStore.account().getServiceIds(), + new GroupCandidateHelper(), new SendGroupUpdateHelper(context)); } @@ -121,8 +124,7 @@ final class GroupManagerV2 { GroupsV2Operations groupsV2Operations, GroupsV2Authorization authorization, GroupsV2StateProcessor groupsV2StateProcessor, - ACI selfAci, - PNI selfPni, + ServiceIds serviceIds, GroupCandidateHelper groupCandidateHelper, SendGroupUpdateHelper sendGroupUpdateHelper) { @@ -132,8 +134,9 @@ final class GroupManagerV2 { this.groupsV2Operations = groupsV2Operations; this.authorization = authorization; this.groupsV2StateProcessor = groupsV2StateProcessor; - this.selfAci = selfAci; - this.selfPni = selfPni; + this.serviceIds = serviceIds; + this.selfAci = serviceIds.getAci(); + this.selfPni = serviceIds.requirePni(); this.groupCandidateHelper = groupCandidateHelper; this.sendGroupUpdateHelper = sendGroupUpdateHelper; } @@ -145,7 +148,7 @@ final class GroupManagerV2 { return groupsV2Api.getGroupJoinInfo(groupSecretParams, Optional.ofNullable(password).map(GroupLinkPassword::serialize), - authorization.getAuthorizationForToday(selfAci, groupSecretParams)); + authorization.getAuthorizationForToday(serviceIds, groupSecretParams)); } @WorkerThread @@ -159,7 +162,7 @@ final class GroupManagerV2 { GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); - return groupsV2Api.getGroupExternalCredential(authorization.getAuthorizationForToday(selfAci, groupSecretParams)); + return groupsV2Api.getGroupExternalCredential(authorization.getAuthorizationForToday(serviceIds, groupSecretParams)); } @WorkerThread @@ -212,7 +215,7 @@ final class GroupManagerV2 { void groupServerQuery(@NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey) throws GroupNotAMemberException, IOException, GroupDoesNotExistException { - new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey) + new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey) .getCurrentGroupStateFromServer(); } @@ -220,7 +223,7 @@ final class GroupManagerV2 { @NonNull DecryptedGroup addedGroupVersion(@NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey) throws GroupNotAMemberException, IOException, GroupDoesNotExistException { - GroupsV2StateProcessor.StateProcessorForGroup stateProcessorForGroup = new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey); + GroupsV2StateProcessor.StateProcessorForGroup stateProcessorForGroup = new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey); DecryptedGroup latest = stateProcessorForGroup.getCurrentGroupStateFromServer(); if (latest.getRevision() == 0) { @@ -291,7 +294,7 @@ final class GroupManagerV2 { } GroupMasterKey masterKey = groupSecretParams.getMasterKey(); - GroupId.V2 groupId = groupDatabase.create(authServiceId, masterKey, decryptedGroup); + GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup); RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromGroupId(groupId); Recipient groupRecipient = Recipient.resolved(groupRecipientId); @@ -344,7 +347,7 @@ final class GroupManagerV2 { Set groupCandidates = groupCandidateHelper.recipientIdsToCandidates(new HashSet<>(newMembers)); if (SignalStore.internalValues().gv2ForceInvites()) { - groupCandidates = GroupCandidate.withoutProfileKeyCredentials(groupCandidates); + groupCandidates = GroupCandidate.withoutExpiringProfileKeyCredentials(groupCandidates); } return commitChangeWithConflictResolution(selfAci, groupOperations.createModifyGroupMembershipChange(groupCandidates, bannedMembers, selfAci.uuid())); @@ -391,7 +394,7 @@ final class GroupManagerV2 { } if (avatarChanged) { - String cdnKey = avatarBytes != null ? groupsV2Api.uploadAvatar(avatarBytes, groupSecretParams, authorization.getAuthorizationForToday(selfAci, groupSecretParams)) + String cdnKey = avatarBytes != null ? groupsV2Api.uploadAvatar(avatarBytes, groupSecretParams, authorization.getAuthorizationForToday(serviceIds, groupSecretParams)) : ""; change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder() .setAvatar(cdnKey)); @@ -524,13 +527,13 @@ final class GroupManagerV2 { GroupCandidate groupCandidate = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId()); - if (!groupCandidate.hasProfileKeyCredential()) { + if (!groupCandidate.hasValidProfileKeyCredential()) { Log.w(TAG, "No credential available, repairing"); ApplicationDependencies.getJobManager().add(new ProfileUploadJob()); return null; } - return commitChangeWithConflictResolution(selfAci, groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.requireProfileKeyCredential())); + return commitChangeWithConflictResolution(selfAci, groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.requireExpiringProfileKeyCredential())); } @WorkerThread @@ -545,14 +548,23 @@ final class GroupManagerV2 { return null; } + Optional aciInPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfAci.uuid()); + Optional pniInPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfPni.uuid()); + GroupCandidate groupCandidate = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId()); - if (!groupCandidate.hasProfileKeyCredential()) { + if (!groupCandidate.hasValidProfileKeyCredential()) { Log.w(TAG, "No credential available"); return null; } - return commitChangeWithConflictResolution(selfAci, groupOperations.createAcceptInviteChange(groupCandidate.requireProfileKeyCredential())); + if (aciInPending.isPresent()) { + return commitChangeWithConflictResolution(selfAci, groupOperations.createAcceptInviteChange(groupCandidate.requireExpiringProfileKeyCredential())); + } else if (pniInPending.isPresent() && FeatureFlags.phoneNumberPrivacy()) { + return commitChangeWithConflictResolution(selfPni, groupOperations.createAcceptPniInviteChange(groupCandidate.requireExpiringProfileKeyCredential())); + } + + throw new GroupChangeFailedException("Unable to accept invite when not in pending list"); } public GroupManager.GroupActionResult ban(UUID uuid) @@ -629,13 +641,19 @@ final class GroupManagerV2 { private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers) throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException { + boolean refetchedAddMemberCredentials = false; change.setSourceUuid(UuidUtil.toByteString(authServiceId.uuid())); for (int attempt = 0; attempt < 5; attempt++) { try { return commitChange(authServiceId, change, allowWhenBlocked, sendToMembers); } catch (GroupPatchNotAcceptedException e) { - throw new GroupChangeFailedException(e); + if (change.getAddMembersCount() > 0 && !refetchedAddMemberCredentials) { + refetchedAddMemberCredentials = true; + change = refetchAddMemberCredentials(change); + } else { + throw new GroupChangeFailedException(e); + } } catch (ConflictException e) { Log.w(TAG, "Invalid group patch or conflict", e); @@ -657,7 +675,7 @@ final class GroupManagerV2 { private GroupChange.Actions.Builder resolveConflict(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change) throws IOException, GroupNotAMemberException, GroupChangeFailedException { - GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(authServiceId, groupMasterKey) + GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(serviceIds, groupMasterKey) .updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis(), null); if (groupUpdateResult.getLatestServer() == null) { @@ -685,6 +703,27 @@ final class GroupManagerV2 { } } + private GroupChange.Actions.Builder refetchAddMemberCredentials(@NonNull GroupChange.Actions.Builder change) { + try { + List ids = groupOperations.decryptAddMembers(change.getAddMembersList()) + .stream() + .map(RecipientId::from) + .collect(java.util.stream.Collectors.toList()); + + for (RecipientId id : ids) { + ProfileUtil.updateExpiringProfileKeyCredential(Recipient.resolved(id)); + } + + List groupCandidates = groupCandidateHelper.recipientIdsToCandidatesList(ids); + + return groupOperations.replaceAddMembers(change, groupCandidates); + } catch (InvalidInputException | VerificationFailedException | IOException e) { + Log.w(TAG, "Unable to refetch credentials for added members, failing change", e); + } + + return change; + } + private GroupManager.GroupActionResult commitChange(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers) throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException { @@ -726,7 +765,7 @@ final class GroupManagerV2 { throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException { try { - return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(authServiceId, groupSecretParams), Optional.empty()); + return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(serviceIds, groupSecretParams), Optional.empty()); } catch (NotInGroupException e) { Log.w(TAG, e); throw new GroupNotAMemberException(e); @@ -751,10 +790,10 @@ final class GroupManagerV2 { } @WorkerThread - void updateLocalToServerRevision(@NonNull ServiceId authServiceId, int revision, long timestamp, @Nullable byte[] signedGroupChange) + void updateLocalToServerRevision(int revision, long timestamp, @Nullable byte[] signedGroupChange) throws IOException, GroupNotAMemberException { - new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey) + new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey) .updateLocalGroupToRevision(revision, timestamp, getDecryptedGroupChange(signedGroupChange)); } @@ -792,10 +831,10 @@ final class GroupManagerV2 { if (SignalStore.internalValues().gv2ForceInvites()) { Log.w(TAG, "Forcing GV2 invites due to internal setting"); - candidates = GroupCandidate.withoutProfileKeyCredentials(candidates); + candidates = GroupCandidate.withoutExpiringProfileKeyCredentials(candidates); } - if (!self.hasProfileKeyCredential()) { + if (!self.hasValidProfileKeyCredential()) { Log.w(TAG, "Cannot create a V2 group as self does not have a versioned profile"); throw new MembershipNotSuitableForV2Exception("Cannot create a V2 group as self does not have a versioned profile"); } @@ -809,9 +848,9 @@ final class GroupManagerV2 { disappearingMessageTimerSeconds); try { - groupsV2Api.putNewGroup(newGroup, authorization.getAuthorizationForToday(selfAci, groupSecretParams)); + groupsV2Api.putNewGroup(newGroup, authorization.getAuthorizationForToday(serviceIds, groupSecretParams)); - DecryptedGroup decryptedGroup = groupsV2Api.getGroup(groupSecretParams, ApplicationDependencies.getGroupsV2Authorization().getAuthorizationForToday(selfAci, groupSecretParams)); + DecryptedGroup decryptedGroup = groupsV2Api.getGroup(groupSecretParams, ApplicationDependencies.getGroupsV2Authorization().getAuthorizationForToday(serviceIds, groupSecretParams)); if (decryptedGroup == null) { throw new GroupChangeFailedException(); } @@ -907,7 +946,7 @@ final class GroupManagerV2 { groupDatabase.update(groupId, updatedGroup); } else { - groupDatabase.create(selfAci, groupMasterKey, decryptedGroup); + groupDatabase.create(groupMasterKey, decryptedGroup); Log.i(TAG, "Created local group with placeholder"); } @@ -951,7 +990,7 @@ final class GroupManagerV2 { throws GroupChangeFailedException, IOException { try { - new GroupsV2StateProcessor(context).forGroup(selfAci, groupMasterKey) + new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey) .updateLocalGroupToRevision(decryptedChange.getRevision(), System.currentTimeMillis(), decryptedChange); @@ -1030,14 +1069,14 @@ final class GroupManagerV2 { GroupCandidate self = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId()); - if (!self.hasProfileKeyCredential()) { + if (!self.hasValidProfileKeyCredential()) { throw new MembershipNotSuitableForV2Exception("No profile key credential for self"); } - ProfileKeyCredential profileKeyCredential = self.requireProfileKeyCredential(); + ExpiringProfileKeyCredential expiringProfileKeyCredential = self.requireExpiringProfileKeyCredential(); - GroupChange.Actions.Builder change = requestToJoin ? groupOperations.createGroupJoinRequest(profileKeyCredential) - : groupOperations.createGroupJoinDirect(profileKeyCredential); + GroupChange.Actions.Builder change = requestToJoin ? groupOperations.createGroupJoinRequest(expiringProfileKeyCredential) + : groupOperations.createGroupJoinDirect(expiringProfileKeyCredential); change.setSourceUuid(selfAci.toByteString()); @@ -1083,7 +1122,7 @@ final class GroupManagerV2 { throws GroupChangeFailedException, IOException, GroupLinkNotActiveException { try { - return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(selfAci, groupSecretParams), Optional.ofNullable(password).map(GroupLinkPassword::serialize)); + return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(serviceIds, groupSecretParams), Optional.ofNullable(password).map(GroupLinkPassword::serialize)); } catch (NotInGroupException | VerificationFailedException e) { Log.w(TAG, e); throw new GroupChangeFailedException(e); @@ -1127,7 +1166,7 @@ final class GroupManagerV2 { throws IOException, VerificationFailedException, InvalidGroupStateException { try { - groupsV2Api.getGroup(groupSecretParams, authorization.getAuthorizationForToday(selfAci, groupSecretParams)); + groupsV2Api.getGroup(groupSecretParams, authorization.getAuthorizationForToday(serviceIds, groupSecretParams)); return true; } catch (NotInGroupException ex) { return false; diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java index 73c610b19..752d64151 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java @@ -72,7 +72,7 @@ public final class GroupsV1MigrationUtil { throw new InvalidMigrationStateException(); } - switch (GroupManager.v2GroupStatus(context, SignalStore.account().getAci(), gv2MasterKey)) { + switch (GroupManager.v2GroupStatus(context, SignalStore.account().requireAci(), gv2MasterKey)) { case DOES_NOT_EXIST: Log.i(TAG, "Group does not exist on the service."); @@ -186,7 +186,7 @@ public final class GroupsV1MigrationUtil { Log.i(TAG, "[Local] Applying all changes since V" + decryptedGroup.getRevision()); try { - GroupManager.updateGroupFromServer(context, SignalStore.account().requireAci(), gv1Id.deriveV2MigrationMasterKey(), LATEST, System.currentTimeMillis(), null); + GroupManager.updateGroupFromServer(context, gv1Id.deriveV2MigrationMasterKey(), LATEST, System.currentTimeMillis(), null); } catch (GroupChangeBusyException | GroupNotAMemberException e) { Log.w(TAG, e); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2Authorization.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2Authorization.java index 60961b642..c59c82459 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2Authorization.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2Authorization.java @@ -4,69 +4,53 @@ import androidx.annotation.NonNull; import org.signal.core.util.logging.Log; import org.signal.libsignal.zkgroup.VerificationFailedException; -import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse; +import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse; import org.signal.libsignal.zkgroup.groups.GroupSecretParams; -import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException; -import org.whispersystems.signalservice.api.push.ServiceId; +import org.whispersystems.signalservice.api.push.ServiceIds; import java.io.IOException; import java.util.Map; -import java.util.Objects; import java.util.concurrent.TimeUnit; public class GroupsV2Authorization { private static final String TAG = Log.tag(GroupsV2Authorization.class); - private final ValueCache aciCache; - private final ValueCache pniCache; + private final ValueCache authCache; private final GroupsV2Api groupsV2Api; - public GroupsV2Authorization(@NonNull GroupsV2Api groupsV2Api, @NonNull ValueCache aciCache, @NonNull ValueCache pniCache) { + public GroupsV2Authorization(@NonNull GroupsV2Api groupsV2Api, @NonNull ValueCache authCache) { this.groupsV2Api = groupsV2Api; - this.aciCache = aciCache; - this.pniCache = pniCache; + this.authCache = authCache; } - public GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ServiceId authServiceId, + public GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ServiceIds serviceIds, @NonNull GroupSecretParams groupSecretParams) throws IOException, VerificationFailedException { - boolean isPni = Objects.equals(authServiceId, SignalStore.account().getPni()); - ValueCache cache = isPni ? pniCache : aciCache; + final long today = currentDaySeconds(); - return getAuthorizationForToday(authServiceId, cache, groupSecretParams, !isPni); - } - - private GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ServiceId authServiceId, - @NonNull ValueCache cache, - @NonNull GroupSecretParams groupSecretParams, - boolean isAci) - throws IOException, VerificationFailedException - { - final int today = currentTimeDays(); - - Map credentials = cache.read(); + Map credentials = authCache.read(); try { - return getAuthorization(authServiceId, groupSecretParams, credentials, today); + return getAuthorization(serviceIds, groupSecretParams, credentials, today); } catch (NoCredentialForRedemptionTimeException e) { Log.i(TAG, "Auth out of date, will update auth and try again"); - cache.clear(); + authCache.clear(); } catch (VerificationFailedException e) { Log.w(TAG, "Verification failed, will update auth and try again", e); - cache.clear(); + authCache.clear(); } Log.i(TAG, "Getting new auth credential responses"); - credentials = groupsV2Api.getCredentials(today, isAci); - cache.write(credentials); + credentials = groupsV2Api.getCredentials(today); + authCache.write(credentials); try { - return getAuthorization(authServiceId, groupSecretParams, credentials, today); + return getAuthorization(serviceIds, groupSecretParams, credentials, today); } catch (NoCredentialForRedemptionTimeException e) { Log.w(TAG, "The credentials returned did not include the day requested"); throw new IOException("Failed to get credentials"); @@ -74,35 +58,34 @@ public class GroupsV2Authorization { } public void clear() { - aciCache.clear(); - pniCache.clear(); + authCache.clear(); } - private static int currentTimeDays() { - return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()); + private static long currentDaySeconds() { + return TimeUnit.DAYS.toSeconds(TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis())); } - private GroupsV2AuthorizationString getAuthorization(ServiceId authServiceId, + private GroupsV2AuthorizationString getAuthorization(ServiceIds serviceIds, GroupSecretParams groupSecretParams, - Map credentials, - int today) + Map credentials, + long todaySeconds) throws NoCredentialForRedemptionTimeException, VerificationFailedException { - AuthCredentialResponse authCredentialResponse = credentials.get(today); + AuthCredentialWithPniResponse authCredentialWithPniResponse = credentials.get(todaySeconds); - if (authCredentialResponse == null) { + if (authCredentialWithPniResponse == null) { throw new NoCredentialForRedemptionTimeException(); } - return groupsV2Api.getGroupsV2AuthorizationString(authServiceId, today, groupSecretParams, authCredentialResponse); + return groupsV2Api.getGroupsV2AuthorizationString(serviceIds.getAci(), serviceIds.requirePni(), todaySeconds, groupSecretParams, authCredentialWithPniResponse); } public interface ValueCache { void clear(); - @NonNull Map read(); + @NonNull Map read(); - void write(@NonNull Map values); + void write(@NonNull Map values); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2AuthorizationMemoryValueCache.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2AuthorizationMemoryValueCache.java index 46506418e..bce7c614d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2AuthorizationMemoryValueCache.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2AuthorizationMemoryValueCache.java @@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.groups; import androidx.annotation.NonNull; -import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse; +import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse; import java.util.Collections; import java.util.HashMap; @@ -10,8 +10,8 @@ import java.util.Map; public final class GroupsV2AuthorizationMemoryValueCache implements GroupsV2Authorization.ValueCache { - private final GroupsV2Authorization.ValueCache inner; - private Map values; + private final GroupsV2Authorization.ValueCache inner; + private Map values; public GroupsV2AuthorizationMemoryValueCache(@NonNull GroupsV2Authorization.ValueCache inner) { this.inner = inner; @@ -24,11 +24,11 @@ public final class GroupsV2AuthorizationMemoryValueCache implements GroupsV2Auth } @Override - public @NonNull synchronized Map read() { - Map map = values; + public @NonNull synchronized Map read() { + Map map = values; if (map == null) { - map = inner.read(); + map = inner.read(); values = map; } @@ -36,7 +36,7 @@ public final class GroupsV2AuthorizationMemoryValueCache implements GroupsV2Auth } @Override - public synchronized void write(@NonNull Map values) { + public synchronized void write(@NonNull Map values) { inner.write(values); this.values = Collections.unmodifiableMap(new HashMap<>(values)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupCandidateHelper.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupCandidateHelper.java index 683ded546..38dc1c687 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupCandidateHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupCandidateHelper.java @@ -1,27 +1,25 @@ package org.thoughtcrime.securesms.groups.v2; -import android.content.Context; - import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import org.signal.core.util.logging.Log; -import org.signal.libsignal.zkgroup.profiles.ProfileKey; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; -import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.ProfileUtil; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.groupsv2.GroupCandidate; import org.whispersystems.signalservice.api.push.ServiceId; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; -import java.util.Locale; +import java.util.List; import java.util.Optional; import java.util.Set; @@ -29,7 +27,7 @@ public class GroupCandidateHelper { private final SignalServiceAccountManager signalServiceAccountManager; private final RecipientDatabase recipientDatabase; - public GroupCandidateHelper(@NonNull Context context) { + public GroupCandidateHelper() { signalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager(); recipientDatabase = SignalDatabase.recipients(); } @@ -52,27 +50,17 @@ public class GroupCandidateHelper { throw new AssertionError("Non UUID members should have need detected by now"); } - Optional profileKeyCredential = Optional.ofNullable(recipient.getProfileKeyCredential()); - GroupCandidate candidate = new GroupCandidate(serviceId.uuid(), profileKeyCredential); + Optional expiringProfileKeyCredential = Optional.ofNullable(recipient.getExpiringProfileKeyCredential()); + GroupCandidate candidate = new GroupCandidate(serviceId.uuid(), expiringProfileKeyCredential); - if (!candidate.hasProfileKeyCredential()) { - ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey()); + if (!candidate.hasValidProfileKeyCredential()) { + recipientDatabase.clearProfileKeyCredential(recipient.getId()); - if (profileKey != null) { - Log.i(TAG, String.format("No profile key credential on recipient %s, fetching", recipient.getId())); - - Optional profileKeyCredentialOptional = signalServiceAccountManager.resolveProfileKeyCredential(serviceId, profileKey, Locale.getDefault()); - - if (profileKeyCredentialOptional.isPresent()) { - boolean updatedProfileKey = recipientDatabase.setProfileKeyCredential(recipient.getId(), profileKey, profileKeyCredentialOptional.get()); - - if (!updatedProfileKey) { - Log.w(TAG, String.format("Failed to update the profile key credential on recipient %s", recipient.getId())); - } else { - Log.i(TAG, String.format("Got new profile key credential for recipient %s", recipient.getId())); - candidate = candidate.withProfileKeyCredential(profileKeyCredentialOptional.get()); - } - } + Optional credential = ProfileUtil.updateExpiringProfileKeyCredential(recipient); + if (credential.isPresent()) { + candidate = candidate.withExpiringProfileKeyCredential(credential.get()); + } else { + candidate = candidate.withoutExpiringProfileKeyCredential(); } } @@ -91,4 +79,17 @@ public class GroupCandidateHelper { return result; } + + @WorkerThread + public @NonNull List recipientIdsToCandidatesList(@NonNull Collection recipientIds) + throws IOException + { + List result = new ArrayList<>(recipientIds.size()); + + for (RecipientId recipientId : recipientIds) { + result.add(recipientIdToCandidate(recipientId)); + } + + return result; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java index 5d5048f6c..a0d8dc4c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java @@ -54,6 +54,7 @@ import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException; import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException; import org.whispersystems.signalservice.api.groupsv2.PartialDecryptedGroup; import org.whispersystems.signalservice.api.push.ServiceId; +import org.whispersystems.signalservice.api.push.ServiceIds; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.push.exceptions.GroupNotFoundException; import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException; @@ -103,10 +104,10 @@ public class GroupsV2StateProcessor { this.groupDatabase = SignalDatabase.groups(); } - public StateProcessorForGroup forGroup(@NonNull ServiceId serviceId, @NonNull GroupMasterKey groupMasterKey) { - ProfileAndMessageHelper profileAndMessageHelper = new ProfileAndMessageHelper(context, serviceId, groupMasterKey, GroupId.v2(groupMasterKey), recipientDatabase); + public StateProcessorForGroup forGroup(@NonNull ServiceIds serviceIds, @NonNull GroupMasterKey groupMasterKey) { + ProfileAndMessageHelper profileAndMessageHelper = new ProfileAndMessageHelper(context, serviceIds.getAci(), groupMasterKey, GroupId.v2(groupMasterKey), recipientDatabase); - return new StateProcessorForGroup(serviceId, context, groupDatabase, groupsV2Api, groupsV2Authorization, groupMasterKey, profileAndMessageHelper); + return new StateProcessorForGroup(serviceIds, context, groupDatabase, groupsV2Api, groupsV2Authorization, groupMasterKey, profileAndMessageHelper); } public enum GroupState { @@ -140,7 +141,7 @@ public class GroupsV2StateProcessor { } public static final class StateProcessorForGroup { - private final ServiceId serviceId; + private final ServiceIds serviceIds; private final Context context; private final GroupDatabase groupDatabase; private final GroupsV2Api groupsV2Api; @@ -150,7 +151,7 @@ public class GroupsV2StateProcessor { private final GroupSecretParams groupSecretParams; private final ProfileAndMessageHelper profileAndMessageHelper; - @VisibleForTesting StateProcessorForGroup(@NonNull ServiceId serviceId, + @VisibleForTesting StateProcessorForGroup(@NonNull ServiceIds serviceIds, @NonNull Context context, @NonNull GroupDatabase groupDatabase, @NonNull GroupsV2Api groupsV2Api, @@ -158,7 +159,7 @@ public class GroupsV2StateProcessor { @NonNull GroupMasterKey groupMasterKey, @NonNull ProfileAndMessageHelper profileAndMessageHelper) { - this.serviceId = serviceId; + this.serviceIds = serviceIds; this.context = context; this.groupDatabase = groupDatabase; this.groupsV2Api = groupsV2Api; @@ -196,17 +197,17 @@ public class GroupsV2StateProcessor { { if (notInGroupAndNotBeingAdded(localRecord, signedGroupChange) && notHavingInviteRevoked(signedGroupChange)) { - Log.w(TAG, "Ignoring P2P group change because we're not currently in the group and this change doesn't add us in. Falling back to a server fetch."); + warn("Ignoring P2P group change because we're not currently in the group and this change doesn't add us in. Falling back to a server fetch."); } else if (SignalStore.internalValues().gv2IgnoreP2PChanges()) { - Log.w(TAG, "Ignoring P2P group change by setting"); + warn( "Ignoring P2P group change by setting"); } else { try { - Log.i(TAG, "Applying P2P group change"); + info("Applying P2P group change"); DecryptedGroup newState = DecryptedGroupUtil.apply(localState, signedGroupChange); inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(newState, signedGroupChange))); } catch (NotAbleToApplyGroupV2ChangeException e) { - Log.w(TAG, "Unable to apply P2P group change", e); + warn( "Unable to apply P2P group change", e); } } } @@ -218,24 +219,24 @@ public class GroupsV2StateProcessor { if (localState != null && signedGroupChange != null) { try { if (notInGroupAndNotBeingAdded(localRecord, signedGroupChange)) { - Log.w(TAG, "Server says we're not a member. Ignoring P2P group change because we're not currently in the group and this change doesn't add us in."); + warn( "Server says we're not a member. Ignoring P2P group change because we're not currently in the group and this change doesn't add us in."); } else { - Log.i(TAG, "Server says we're not a member. Applying P2P group change."); + info("Server says we're not a member. Applying P2P group change."); DecryptedGroup newState = DecryptedGroupUtil.applyWithoutRevisionCheck(localState, signedGroupChange); inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(newState, signedGroupChange))); } } catch (NotAbleToApplyGroupV2ChangeException failed) { - Log.w(TAG, "Unable to apply P2P group change when not a member", failed); + warn( "Unable to apply P2P group change when not a member", failed); } } if (inputGroupState == null) { - if (localState != null && DecryptedGroupUtil.isPendingOrRequesting(localState, serviceId.uuid())) { - Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, but we think we are a pending or requesting member"); + if (localState != null && DecryptedGroupUtil.isPendingOrRequesting(localState, serviceIds)) { + warn( "Unable to query server for group " + groupId + " server says we're not in group, but we think we are a pending or requesting member"); throw new GroupNotAMemberException(e, true); } else { - Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message"); + warn( "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message"); insertGroupLeave(); } throw e; @@ -252,7 +253,7 @@ public class GroupsV2StateProcessor { updateLocalDatabaseGroupState(inputGroupState, newLocalState); if (localState != null && localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) { - Log.i(TAG, "Inserting single update message for restore placeholder"); + info("Inserting single update message for restore placeholder"); profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(newLocalState, null))); } else { profileAndMessageHelper.insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries()); @@ -261,7 +262,7 @@ public class GroupsV2StateProcessor { GlobalGroupState remainingWork = advanceGroupStateResult.getNewGlobalGroupState(); if (remainingWork.getServerHistory().size() > 0) { - Log.i(TAG, String.format(Locale.US, "There are more revisions on the server for this group, scheduling for later, V[%d..%d]", newLocalState.getRevision() + 1, remainingWork.getLatestRevisionNumber())); + info(String.format(Locale.US, "There are more revisions on the server for this group, scheduling for later, V[%d..%d]", newLocalState.getRevision() + 1, remainingWork.getLatestRevisionNumber())); ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId, remainingWork.getLatestRevisionNumber())); } @@ -276,14 +277,14 @@ public class GroupsV2StateProcessor { .map(DecryptedMember::getUuid) .map(UuidUtil::fromByteStringOrNull) .filter(Objects::nonNull) - .anyMatch(u -> u.equals(serviceId.uuid())); + .anyMatch(serviceIds::matches); boolean addedAsPendingMember = signedGroupChange.getNewPendingMembersList() .stream() .map(DecryptedPendingMember::getUuid) .map(UuidUtil::fromByteStringOrNull) .filter(Objects::nonNull) - .anyMatch(u -> u.equals(serviceId.uuid())); + .anyMatch(serviceIds::matches); return !currentlyInGroup && !addedAsMember && !addedAsPendingMember; } @@ -294,7 +295,7 @@ public class GroupsV2StateProcessor { .map(DecryptedPendingMemberRemoval::getUuid) .map(UuidUtil::fromByteStringOrNull) .filter(Objects::nonNull) - .anyMatch(u -> u.equals(serviceId.uuid())); + .anyMatch(serviceIds::matches); return !havingInviteRevoked; } @@ -305,44 +306,43 @@ public class GroupsV2StateProcessor { private GroupUpdateResult updateLocalGroupFromServerPaged(int revision, DecryptedGroup localState, long timestamp, boolean forceIncludeFirst) throws IOException, GroupNotAMemberException { boolean latestRevisionOnly = revision == LATEST && (localState == null || localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION); - Log.i(TAG, "Paging from server revision: " + (revision == LATEST ? "latest" : revision) + ", latestOnly: " + latestRevisionOnly); + info("Paging from server revision: " + (revision == LATEST ? "latest" : revision) + ", latestOnly: " + latestRevisionOnly); PartialDecryptedGroup latestServerGroup; GlobalGroupState inputGroupState; try { - latestServerGroup = groupsV2Api.getPartialDecryptedGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams)); + latestServerGroup = groupsV2Api.getPartialDecryptedGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams)); } catch (NotInGroupException | GroupNotFoundException e) { throw new GroupNotAMemberException(e); } catch (VerificationFailedException | InvalidGroupStateException e) { throw new IOException(e); } - if (localState != null && localState.getRevision() >= latestServerGroup.getRevision() && GroupProtoUtil.isMember(serviceId.uuid(), localState.getMembersList())) { - Log.i(TAG, "Local state is at or later than server"); + if (localState != null && localState.getRevision() >= latestServerGroup.getRevision() && GroupProtoUtil.isMember(serviceIds.getAci().uuid(), localState.getMembersList())) { + info("Local state is at or later than server"); return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null); } - if (latestRevisionOnly || !GroupProtoUtil.isMember(serviceId.uuid(), latestServerGroup.getMembersList())) { - Log.i(TAG, "Latest revision or not a member, use latest only"); + if (latestRevisionOnly || !GroupProtoUtil.isMember(serviceIds.getAci().uuid(), latestServerGroup.getMembersList())) { + info("Latest revision or not a member, use latest only"); inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(latestServerGroup.getFullyDecryptedGroup(), null))); } else { - int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, serviceId.uuid()); + int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, serviceIds.getAci().uuid()); int logsNeededFrom = localState != null ? Math.max(localState.getRevision(), revisionWeWereAdded) : revisionWeWereAdded; boolean includeFirstState = forceIncludeFirst || localState == null || localState.getRevision() < 0 || localState.getRevision() == revisionWeWereAdded || - !GroupProtoUtil.isMember(serviceId.uuid(), localState.getMembersList()) || + !GroupProtoUtil.isMember(serviceIds.getAci().uuid(), localState.getMembersList()) || (revision == LATEST && localState.getRevision() + 1 < latestServerGroup.getRevision()); - Log.i(TAG, - "Requesting from server currentRevision: " + (localState != null ? localState.getRevision() : "null") + - " logsNeededFrom: " + logsNeededFrom + - " includeFirstState: " + includeFirstState + - " forceIncludeFirst: " + forceIncludeFirst); - inputGroupState = getFullMemberHistoryPage(localState, serviceId, logsNeededFrom, includeFirstState); + info("Requesting from server currentRevision: " + (localState != null ? localState.getRevision() : "null") + + " logsNeededFrom: " + logsNeededFrom + + " includeFirstState: " + includeFirstState + + " forceIncludeFirst: " + forceIncludeFirst); + inputGroupState = getFullMemberHistoryPage(localState, logsNeededFrom, includeFirstState); } ProfileKeySet profileKeys = new ProfileKeySet(); @@ -354,13 +354,13 @@ public class GroupsV2StateProcessor { while (hasMore) { AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(inputGroupState, revision); DecryptedGroup newLocalState = advanceGroupStateResult.getNewGlobalGroupState().getLocalState(); - Log.i(TAG, "Advanced group to revision: " + (newLocalState != null ? newLocalState.getRevision() : "null")); + info("Advanced group to revision: " + (newLocalState != null ? newLocalState.getRevision() : "null")); if (newLocalState != null && !inputGroupState.hasMore() && !forceIncludeFirst) { int newLocalRevision = newLocalState.getRevision(); int requestRevision = (revision == LATEST) ? latestServerGroup.getRevision() : revision; if (newLocalRevision < requestRevision) { - Log.w(TAG, "Paging again with force first snapshot enabled due to error processing changes. New local revision [" + newLocalRevision + "] hasn't reached our desired level [" + requestRevision + "]"); + warn( "Paging again with force first snapshot enabled due to error processing changes. New local revision [" + newLocalRevision + "] hasn't reached our desired level [" + requestRevision + "]"); return updateLocalGroupFromServerPaged(revision, localState, timestamp, true); } } @@ -389,20 +389,20 @@ public class GroupsV2StateProcessor { hasMore = inputGroupState.hasMore(); if (hasMore) { - Log.i(TAG, "Request next page from server revision: " + finalState.getRevision() + " nextPageRevision: " + inputGroupState.getNextPageRevision()); - inputGroupState = getFullMemberHistoryPage(finalState, serviceId, inputGroupState.getNextPageRevision(), false); + info("Request next page from server revision: " + finalState.getRevision() + " nextPageRevision: " + inputGroupState.getNextPageRevision()); + inputGroupState = getFullMemberHistoryPage(finalState, inputGroupState.getNextPageRevision(), false); } } if (localState != null && localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) { - Log.i(TAG, "Inserting single update message for restore placeholder"); + info("Inserting single update message for restore placeholder"); profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(finalState, null))); } profileAndMessageHelper.persistLearnedProfileKeys(profileKeys); if (finalGlobalGroupState.getServerHistory().size() > 0) { - Log.i(TAG, String.format(Locale.US, "There are more revisions on the server for this group, scheduling for later, V[%d..%d]", finalState.getRevision() + 1, finalGlobalGroupState.getLatestRevisionNumber())); + info(String.format(Locale.US, "There are more revisions on the server for this group, scheduling for later, V[%d..%d]", finalState.getRevision() + 1, finalGlobalGroupState.getLatestRevisionNumber())); ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId, finalGlobalGroupState.getLatestRevisionNumber())); } @@ -414,7 +414,7 @@ public class GroupsV2StateProcessor { throws IOException, GroupNotAMemberException, GroupDoesNotExistException { try { - return groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams)); + return groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams)); } catch (GroupNotFoundException e) { throw new GroupDoesNotExistException(e); } catch (NotInGroupException e) { @@ -429,7 +429,7 @@ public class GroupsV2StateProcessor { throws IOException, GroupNotAMemberException, GroupDoesNotExistException { try { - return groupsV2Api.getGroupHistoryPage(groupSecretParams, revision, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams), true) + return groupsV2Api.getGroupHistoryPage(groupSecretParams, revision, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams), true) .getResults() .get(0) .getGroup() @@ -445,12 +445,12 @@ public class GroupsV2StateProcessor { private void insertGroupLeave() { if (!groupDatabase.isActive(groupId)) { - Log.w(TAG, "Group has already been left."); + warn("Group has already been left."); return; } Recipient groupRecipient = Recipient.externalGroupExact(groupId); - UUID selfUuid = serviceId.uuid(); + UUID selfUuid = serviceIds.getAci().uuid(); DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId) .requireV2GroupProperties() @@ -485,7 +485,7 @@ public class GroupsV2StateProcessor { mmsDatabase.markAsSent(id, true); threadDatabase.update(threadId, false, false); } catch (MmsException e) { - Log.w(TAG, "Failed to insert leave message.", e); + warn( "Failed to insert leave message.", e); } groupDatabase.setActive(groupId, false); @@ -509,7 +509,7 @@ public class GroupsV2StateProcessor { boolean needsAvatarFetch; if (inputGroupState.getLocalState() == null) { - groupDatabase.create(serviceId, masterKey, newLocalState); + groupDatabase.create(masterKey, newLocalState); needsAvatarFetch = !TextUtils.isEmpty(newLocalState.getAvatar()); } else { groupDatabase.update(masterKey, newLocalState); @@ -523,14 +523,14 @@ public class GroupsV2StateProcessor { profileAndMessageHelper.determineProfileSharing(inputGroupState, newLocalState); } - private GlobalGroupState getFullMemberHistoryPage(DecryptedGroup localState, @NonNull ServiceId serviceId, int logsNeededFromRevision, boolean includeFirstState) throws IOException { + private GlobalGroupState getFullMemberHistoryPage(DecryptedGroup localState, int logsNeededFromRevision, boolean includeFirstState) throws IOException { try { - GroupHistoryPage groupHistoryPage = groupsV2Api.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams), includeFirstState); + GroupHistoryPage groupHistoryPage = groupsV2Api.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams), includeFirstState); ArrayList history = new ArrayList<>(groupHistoryPage.getResults().size()); boolean ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges(); if (ignoreServerChanges) { - Log.w(TAG, "Server change logs are ignored by setting"); + warn( "Server change logs are ignored by setting"); } for (DecryptedGroupHistoryEntry entry : groupHistoryPage.getResults()) { @@ -547,6 +547,22 @@ public class GroupsV2StateProcessor { throw new IOException(e); } } + + private void info(String message) { + info(message, null); + } + + private void info(String message, Throwable t) { + Log.i(TAG, "[" + groupId.toString() + "] " + message, t); + } + + private void warn(String message) { + warn(message, null); + } + + private void warn(String message, Throwable e) { + Log.w(TAG, "[" + groupId.toString() + "] " + message, e); + } } @VisibleForTesting @@ -615,7 +631,7 @@ public class GroupsV2StateProcessor { .map(uuid -> Recipient.externalPush(ServiceId.from(uuid)))); if (addedBy.isPresent() && addedBy.get().isBlocked()) { - Log.i(TAG, String.format( "Added to group %s by a blocked user %s. Leaving group.", groupId, addedBy.get().getId())); + Log.i(TAG, String.format("Added to group %s by a blocked user %s. Leaving group.", groupId, addedBy.get().getId())); ApplicationDependencies.getJobManager().add(new LeaveGroupV2Job(groupId)); //noinspection UnnecessaryReturnStatement return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java index bb74dcd37..ed35b60d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java @@ -6,8 +6,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; import org.thoughtcrime.securesms.badges.BadgeRepository; import org.thoughtcrime.securesms.badges.Badges; import org.thoughtcrime.securesms.badges.models.Badge; @@ -32,13 +32,13 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription; +import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil; import org.whispersystems.signalservice.internal.ServiceResponse; import java.io.IOException; import java.util.Comparator; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -130,26 +130,23 @@ public class RefreshOwnProfileJob extends BaseJob { setProfileBadges(profile.getBadges()); ensureUnidentifiedAccessCorrect(profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess()); - Optional profileKeyCredential = profileAndCredential.getProfileKeyCredential(); - if (profileKeyCredential.isPresent()) { - setProfileKeyCredential(self, ProfileKeyUtil.getSelfProfileKey(), profileKeyCredential.get()); - } + profileAndCredential.getExpiringProfileKeyCredential() + .ifPresent(expiringProfileKeyCredential -> setExpiringProfileKeyCredential(self, ProfileKeyUtil.getSelfProfileKey(), expiringProfileKeyCredential)); StoryOnboardingDownloadJob.Companion.enqueueIfNeeded(); } - private void setProfileKeyCredential(@NonNull Recipient recipient, - @NonNull ProfileKey recipientProfileKey, - @NonNull ProfileKeyCredential credential) + private void setExpiringProfileKeyCredential(@NonNull Recipient recipient, + @NonNull ProfileKey recipientProfileKey, + @NonNull ExpiringProfileKeyCredential credential) { RecipientDatabase recipientDatabase = SignalDatabase.recipients(); recipientDatabase.setProfileKeyCredential(recipient.getId(), recipientProfileKey, credential); } private static SignalServiceProfile.RequestType getRequestType(@NonNull Recipient recipient) { - return !recipient.hasProfileKeyCredential() - ? SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL - : SignalServiceProfile.RequestType.PROFILE; + return ExpiringProfileCredentialUtil.isValid(recipient.getExpiringProfileKeyCredential()) ? SignalServiceProfile.RequestType.PROFILE + : SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL; } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoWorkerJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoWorkerJob.java index d7c44f940..97c92e767 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoWorkerJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoWorkerJob.java @@ -4,7 +4,6 @@ import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import org.signal.core.util.logging.Log; -import org.signal.libsignal.zkgroup.VerificationFailedException; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.groups.GroupChangeBusyException; @@ -15,10 +14,8 @@ import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException; -import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import java.io.IOException; @@ -91,24 +88,7 @@ final class RequestGroupV2InfoWorkerJob extends BaseJob { return; } - ServiceId authServiceId = group.get().getAuthServiceId() != null ? group.get().getAuthServiceId() : SignalStore.account().requireAci(); - - try { - GroupManager.updateGroupFromServer(context, authServiceId, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null); - } catch (GroupNotAMemberException | IOException e) { - ServiceId otherServiceId = authServiceId.equals(SignalStore.account().getPni()) ? SignalStore.account().getAci() : SignalStore.account().getPni(); - boolean isNotAMemberOrPending = e instanceof GroupNotAMemberException && !((GroupNotAMemberException) e).isLikelyPendingMember(); - boolean verificationFailed = e.getCause() instanceof VerificationFailedException; - - if (otherServiceId != null && (isNotAMemberOrPending || verificationFailed)) { - Log.i(TAG, "Request failed, attempting with other id"); - GroupManager.updateGroupFromServer(context, otherServiceId, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null); - Log.i(TAG, "Request succeeded with other credential. Associating " + otherServiceId + " with group " + groupId); - SignalDatabase.groups().setAuthServiceId(otherServiceId, groupId); - } else { - throw e; - } - } + GroupManager.updateGroupFromServer(context, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index 829357af2..7595ca2a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -12,13 +12,14 @@ import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import org.signal.core.util.ListUtil; +import org.signal.core.util.SetUtil; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.util.Pair; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; import org.thoughtcrime.securesms.badges.Badges; import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; @@ -41,7 +42,6 @@ import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.ProfileUtil; -import org.signal.core.util.SetUtil; import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; @@ -50,6 +50,7 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.services.ProfileService; +import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil; import org.whispersystems.signalservice.internal.ServiceResponse; import java.io.IOException; @@ -59,7 +60,6 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -274,7 +274,7 @@ public class RetrieveProfileJob extends BaseJob { } else if (processor.genericIoError()) { state.retries.add(recipient.getId()); } else { - Log.w(TAG, "Failed to retrieve profile for " + recipient.getId()); + Log.w(TAG, "Failed to retrieve profile for " + recipient.getId(), processor.getError()); } return state; }) @@ -350,10 +350,8 @@ public class RetrieveProfileJob extends BaseJob { setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess()); if (recipientProfileKey != null) { - Optional profileKeyCredential = profileAndCredential.getProfileKeyCredential(); - if (profileKeyCredential.isPresent()) { - setProfileKeyCredential(recipient, recipientProfileKey, profileKeyCredential.get()); - } + profileAndCredential.getExpiringProfileKeyCredential() + .ifPresent(profileKeyCredential -> setExpiringProfileKeyCredential(recipient, recipientProfileKey, profileKeyCredential)); } } @@ -371,18 +369,17 @@ public class RetrieveProfileJob extends BaseJob { SignalDatabase.recipients().setBadges(recipient.getId(), badges); } - private void setProfileKeyCredential(@NonNull Recipient recipient, - @NonNull ProfileKey recipientProfileKey, - @NonNull ProfileKeyCredential credential) + private void setExpiringProfileKeyCredential(@NonNull Recipient recipient, + @NonNull ProfileKey recipientProfileKey, + @NonNull ExpiringProfileKeyCredential credential) { RecipientDatabase recipientDatabase = SignalDatabase.recipients(); recipientDatabase.setProfileKeyCredential(recipient.getId(), recipientProfileKey, credential); } private static SignalServiceProfile.RequestType getRequestType(@NonNull Recipient recipient) { - return !recipient.hasProfileKeyCredential() - ? SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL - : SignalServiceProfile.RequestType.PROFILE; + return ExpiringProfileCredentialUtil.isValid(recipient.getExpiringProfileKeyCredential()) ? SignalServiceProfile.RequestType.PROFILE + : SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL; } private void setIdentityKey(Recipient recipient, String identityKeyValue) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt index 46517665d..c753ef0d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.api.push.ACI import org.whispersystems.signalservice.api.push.PNI +import org.whispersystems.signalservice.api.push.ServiceIds import org.whispersystems.signalservice.api.push.SignalServiceAddress import java.security.SecureRandom diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/GroupsV2AuthorizationSignalStoreCache.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/GroupsV2AuthorizationSignalStoreCache.java index 068570c7e..f9d7ddfe9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/GroupsV2AuthorizationSignalStoreCache.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/GroupsV2AuthorizationSignalStoreCache.java @@ -7,7 +7,7 @@ import com.google.protobuf.InvalidProtocolBufferException; import org.signal.core.util.logging.Log; import org.signal.libsignal.zkgroup.InvalidInputException; -import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse; +import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse; import org.thoughtcrime.securesms.database.model.databaseprotos.TemporalAuthCredentialResponse; import org.thoughtcrime.securesms.database.model.databaseprotos.TemporalAuthCredentialResponses; import org.thoughtcrime.securesms.groups.GroupsV2Authorization; @@ -21,27 +21,20 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth private static final String TAG = Log.tag(GroupsV2AuthorizationSignalStoreCache.class); - private static final String ACI_PREFIX = "gv2:auth_token_cache"; - private static final int ACI_VERSION = 2; - - private static final String PNI_PREFIX = "gv2:auth_token_cache:pni"; - private static final int PNI_VERSION = 1; + private static final String ACI_PNI_PREFIX = "gv2:auth_token_cache"; + private static final int ACI_PNI_VERSION = 3; private final String key; private final KeyValueStore store; public static GroupsV2AuthorizationSignalStoreCache createAciCache(@NonNull KeyValueStore store) { - if (store.containsKey(ACI_PREFIX)) { + if (store.containsKey(ACI_PNI_PREFIX)) { store.beginWrite() - .remove(ACI_PREFIX) + .remove(ACI_PNI_PREFIX) .commit(); } - return new GroupsV2AuthorizationSignalStoreCache(store, ACI_PREFIX + ":" + ACI_VERSION); - } - - public static GroupsV2AuthorizationSignalStoreCache createPniCache(@NonNull KeyValueStore store) { - return new GroupsV2AuthorizationSignalStoreCache(store, PNI_PREFIX + ":" + PNI_VERSION); + return new GroupsV2AuthorizationSignalStoreCache(store, ACI_PNI_PREFIX + ":" + ACI_PNI_VERSION); } private GroupsV2AuthorizationSignalStoreCache(@NonNull KeyValueStore store, @NonNull String key) { @@ -55,27 +48,27 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth .remove(key) .commit(); - info("Cleared local response cache"); + Log.i(TAG, "Cleared local response cache"); } @Override - public @NonNull Map read() { + public @NonNull Map read() { byte[] credentialBlob = store.getBlob(key, null); if (credentialBlob == null) { - info("No credentials responses are cached locally"); + Log.i(TAG, "No credentials responses are cached locally"); return Collections.emptyMap(); } try { - TemporalAuthCredentialResponses temporalCredentials = TemporalAuthCredentialResponses.parseFrom(credentialBlob); - HashMap result = new HashMap<>(temporalCredentials.getCredentialResponseCount()); + TemporalAuthCredentialResponses temporalCredentials = TemporalAuthCredentialResponses.parseFrom(credentialBlob); + HashMap result = new HashMap<>(temporalCredentials.getCredentialResponseCount()); for (TemporalAuthCredentialResponse credential : temporalCredentials.getCredentialResponseList()) { - result.put(credential.getDate(), new AuthCredentialResponse(credential.getAuthCredentialResponse().toByteArray())); + result.put(credential.getDate(), new AuthCredentialWithPniResponse(credential.getAuthCredentialResponse().toByteArray())); } - info(String.format(Locale.US, "Loaded %d credentials from local storage", result.size())); + Log.i(TAG, String.format(Locale.US, "Loaded %d credentials from local storage", result.size())); return result; } catch (InvalidProtocolBufferException | InvalidInputException e) { @@ -84,10 +77,10 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth } @Override - public void write(@NonNull Map values) { + public void write(@NonNull Map values) { TemporalAuthCredentialResponses.Builder builder = TemporalAuthCredentialResponses.newBuilder(); - for (Map.Entry entry : values.entrySet()) { + for (Map.Entry entry : values.entrySet()) { builder.addCredentialResponse(TemporalAuthCredentialResponse.newBuilder() .setDate(entry.getKey()) .setAuthCredentialResponse(ByteString.copyFrom(entry.getValue().serialize()))); @@ -97,10 +90,6 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth .putBlob(key, builder.build().toByteArray()) .commit(); - info(String.format(Locale.US, "Written %d credentials to local storage", values.size())); - } - - private void info(String message) { - Log.i(TAG, (key.startsWith(PNI_PREFIX) ? "[PNI]" : "[ACI]") + " " + message); + Log.i(TAG, String.format(Locale.US, "Written %d credentials to local storage", values.size())); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ServiceIds.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ServiceIds.kt deleted file mode 100644 index e84f1a4fd..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ServiceIds.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.thoughtcrime.securesms.keyvalue - -import com.google.protobuf.ByteString -import org.whispersystems.signalservice.api.push.ACI -import org.whispersystems.signalservice.api.push.PNI -import org.whispersystems.signalservice.api.util.UuidUtil -import java.util.UUID - -/** - * Helper for dealing with [ServiceId] matching when you only care that either of your - * service ids match but don't care which one. - */ -data class ServiceIds(val aci: ACI, val pni: PNI?) { - - private val aciByteString: ByteString by lazy { UuidUtil.toByteString(aci.uuid()) } - private val pniByteString: ByteString? by lazy { pni?.let { UuidUtil.toByteString(it.uuid()) } } - - fun matches(uuid: UUID): Boolean { - return uuid == aci.uuid() || uuid == pni?.uuid() - } - - fun matches(uuid: ByteString): Boolean { - return uuid == aciByteString || uuid == pniByteString - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java index ebff9ed62..6af50473f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java @@ -265,10 +265,6 @@ public final class SignalStore { return GroupsV2AuthorizationSignalStoreCache.createAciCache(getStore()); } - public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2PniAuthorizationCache() { - return GroupsV2AuthorizationSignalStoreCache.createPniCache(getStore()); - } - public static @NonNull PreferenceDataStore getPreferenceDataStore() { return new SignalPreferenceDataStore(getStore()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index d51de77b7..566a56d2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -544,13 +544,8 @@ public final class MessageContentProcessor { throws IOException, GroupChangeBusyException { try { - ServiceId authServiceId = ServiceId.parseOrNull(content.getDestinationUuid()); - if (authServiceId == null) { - warn(content.getTimestamp(), "Group message missing destination uuid, defaulting to ACI"); - authServiceId = SignalStore.account().requireAci(); - } - long timestamp = groupV2.getSignedGroupChange() != null ? content.getTimestamp() : content.getTimestamp() - 1; - GroupManager.updateGroupFromServer(context, authServiceId, groupV2.getMasterKey(), groupV2.getRevision(), timestamp, groupV2.getSignedGroupChange()); + long timestamp = groupV2.getSignedGroupChange() != null ? content.getTimestamp() : content.getTimestamp() - 1; + GroupManager.updateGroupFromServer(context, groupV2.getMasterKey(), groupV2.getRevision(), timestamp, groupV2.getSignedGroupChange()); return true; } catch (GroupNotAMemberException e) { warn(String.valueOf(content.getTimestamp()), "Ignoring message for a group we're not in"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 4cc06ec6d..0a2c113e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -103,9 +103,10 @@ public class ApplicationMigrations { static final int STORY_DISTRIBUTION_LIST_SYNC = 59; static final int EMOJI_VERSION_7 = 60; static final int MY_STORY_PRIVACY_MODE = 61; + static final int REFRESH_EXPIRING_CREDENTIAL = 62; } - public static final int CURRENT_VERSION = 61; + public static final int CURRENT_VERSION = 62; /** * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call @@ -451,6 +452,10 @@ public class ApplicationMigrations { jobs.put(Version.MY_STORY_PRIVACY_MODE, new SyncDistributionListsMigrationJob()); } + if (lastSeenVersion < Version.REFRESH_EXPIRING_CREDENTIAL) { + jobs.put(Version.REFRESH_EXPIRING_CREDENTIAL, new AttributesMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentRecipientSelectionFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentRecipientSelectionFragment.java index ee8f68d5a..7fbc17d08 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentRecipientSelectionFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentRecipientSelectionFragment.java @@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; import androidx.navigation.Navigation; +import org.signal.core.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.ContactSelectionListFragment; import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; @@ -22,8 +23,8 @@ import org.thoughtcrime.securesms.payments.preferences.model.PayeeParcelable; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.ViewUtil; -import org.signal.core.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.navigation.SafeNavigation; +import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil; import java.util.Optional; import java.util.function.Consumer; @@ -81,7 +82,7 @@ public class PaymentRecipientSelectionFragment extends LoggingFragment implement } @Override - public void onContactDeselected(@NonNull Optional recipientId, @Nullable String number) { } + public void onContactDeselected(@NonNull Optional recipientId, @Nullable String number) {} @Override public void onSelectionChanged() { @@ -98,9 +99,9 @@ public class PaymentRecipientSelectionFragment extends LoggingFragment implement } private void createPaymentOrShowWarningDialog(@NonNull Recipient recipient) { - if (recipient.hasProfileKeyCredential()) { + if (ExpiringProfileCredentialUtil.isValid(recipient.getExpiringProfileKeyCredential())) { createPayment(recipient.getId()); - } else { + } else { showWarningDialog(recipient.getId()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 2223aa6fa..165285536 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -14,7 +14,7 @@ import com.annimon.stream.Stream; import org.signal.core.util.StringUtil; import org.signal.core.util.logging.Log; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; @@ -81,61 +81,61 @@ public class Recipient { private static final int MAX_MEMBER_NAMES = 10; - private final RecipientId id; - private final boolean resolving; - private final ServiceId serviceId; - private final PNI pni; - private final String username; - private final String e164; - private final String email; - private final GroupId groupId; - private final DistributionListId distributionListId; - private final List participants; - private final Optional groupAvatarId; - private final boolean isSelf; - private final boolean blocked; - private final long muteUntil; - private final VibrateState messageVibrate; - private final VibrateState callVibrate; - private final Uri messageRingtone; - private final Uri callRingtone; - private final Optional defaultSubscriptionId; - private final int expireMessages; - private final RegisteredState registered; - private final byte[] profileKey; - private final ProfileKeyCredential profileKeyCredential; - private final String groupName; - private final Uri systemContactPhoto; - private final String customLabel; - private final Uri contactUri; - private final ProfileName signalProfileName; - private final String profileAvatar; - private final boolean hasProfileImage; - private final boolean profileSharing; - private final long lastProfileFetch; - private final String notificationChannel; - private final UnidentifiedAccessMode unidentifiedAccessMode; - private final boolean forceSmsSelection; - private final Capability groupsV1MigrationCapability; - private final Capability senderKeyCapability; - private final Capability announcementGroupCapability; - private final Capability changeNumberCapability; - private final Capability storiesCapability; - private final Capability giftBadgesCapability; - private final InsightsBannerTier insightsBannerTier; - private final byte[] storageId; - private final MentionSetting mentionSetting; - private final ChatWallpaper wallpaper; - private final ChatColors chatColors; - private final AvatarColor avatarColor; - private final String about; - private final String aboutEmoji; - private final ProfileName systemProfileName; - private final String systemContactName; - private final Optional extras; - private final boolean hasGroupsInCommon; - private final List badges; - private final boolean isReleaseNotesRecipient; + private final RecipientId id; + private final boolean resolving; + private final ServiceId serviceId; + private final PNI pni; + private final String username; + private final String e164; + private final String email; + private final GroupId groupId; + private final DistributionListId distributionListId; + private final List participants; + private final Optional groupAvatarId; + private final boolean isSelf; + private final boolean blocked; + private final long muteUntil; + private final VibrateState messageVibrate; + private final VibrateState callVibrate; + private final Uri messageRingtone; + private final Uri callRingtone; + private final Optional defaultSubscriptionId; + private final int expireMessages; + private final RegisteredState registered; + private final byte[] profileKey; + private final ExpiringProfileKeyCredential expiringProfileKeyCredential; + private final String groupName; + private final Uri systemContactPhoto; + private final String customLabel; + private final Uri contactUri; + private final ProfileName signalProfileName; + private final String profileAvatar; + private final boolean hasProfileImage; + private final boolean profileSharing; + private final long lastProfileFetch; + private final String notificationChannel; + private final UnidentifiedAccessMode unidentifiedAccessMode; + private final boolean forceSmsSelection; + private final Capability groupsV1MigrationCapability; + private final Capability senderKeyCapability; + private final Capability announcementGroupCapability; + private final Capability changeNumberCapability; + private final Capability storiesCapability; + private final Capability giftBadgesCapability; + private final InsightsBannerTier insightsBannerTier; + private final byte[] storageId; + private final MentionSetting mentionSetting; + private final ChatWallpaper wallpaper; + private final ChatColors chatColors; + private final AvatarColor avatarColor; + private final String about; + private final String aboutEmoji; + private final ProfileName systemProfileName; + private final String systemContactName; + private final Optional extras; + private final boolean hasGroupsInCommon; + private final List badges; + private final boolean isReleaseNotesRecipient; /** * Returns a {@link LiveRecipient}, which contains a {@link Recipient} that may or may not be @@ -358,119 +358,119 @@ public class Recipient { } Recipient(@NonNull RecipientId id) { - this.id = id; - this.resolving = true; - this.serviceId = null; - this.pni = null; - this.username = null; - this.e164 = null; - this.email = null; - this.groupId = null; - this.distributionListId = null; - this.participants = Collections.emptyList(); - this.groupAvatarId = Optional.empty(); - this.isSelf = false; - this.blocked = false; - this.muteUntil = 0; - this.messageVibrate = VibrateState.DEFAULT; - this.callVibrate = VibrateState.DEFAULT; - this.messageRingtone = null; - this.callRingtone = null; - this.insightsBannerTier = InsightsBannerTier.TIER_TWO; - this.defaultSubscriptionId = Optional.empty(); - this.expireMessages = 0; - this.registered = RegisteredState.UNKNOWN; - this.profileKey = null; - this.profileKeyCredential = null; - this.groupName = null; - this.systemContactPhoto = null; - this.customLabel = null; - this.contactUri = null; - this.signalProfileName = ProfileName.EMPTY; - this.profileAvatar = null; - this.hasProfileImage = false; - this.profileSharing = false; - this.lastProfileFetch = 0; - this.notificationChannel = null; - this.unidentifiedAccessMode = UnidentifiedAccessMode.DISABLED; - this.forceSmsSelection = false; - this.groupsV1MigrationCapability = Capability.UNKNOWN; - this.senderKeyCapability = Capability.UNKNOWN; - this.announcementGroupCapability = Capability.UNKNOWN; - this.changeNumberCapability = Capability.UNKNOWN; - this.storiesCapability = Capability.UNKNOWN; - this.giftBadgesCapability = Capability.UNKNOWN; - this.storageId = null; - this.mentionSetting = MentionSetting.ALWAYS_NOTIFY; - this.wallpaper = null; - this.chatColors = null; - this.avatarColor = AvatarColor.UNKNOWN; - this.about = null; - this.aboutEmoji = null; - this.systemProfileName = ProfileName.EMPTY; - this.systemContactName = null; - this.extras = Optional.empty(); - this.hasGroupsInCommon = false; - this.badges = Collections.emptyList(); - this.isReleaseNotesRecipient = false; + this.id = id; + this.resolving = true; + this.serviceId = null; + this.pni = null; + this.username = null; + this.e164 = null; + this.email = null; + this.groupId = null; + this.distributionListId = null; + this.participants = Collections.emptyList(); + this.groupAvatarId = Optional.empty(); + this.isSelf = false; + this.blocked = false; + this.muteUntil = 0; + this.messageVibrate = VibrateState.DEFAULT; + this.callVibrate = VibrateState.DEFAULT; + this.messageRingtone = null; + this.callRingtone = null; + this.insightsBannerTier = InsightsBannerTier.TIER_TWO; + this.defaultSubscriptionId = Optional.empty(); + this.expireMessages = 0; + this.registered = RegisteredState.UNKNOWN; + this.profileKey = null; + this.expiringProfileKeyCredential = null; + this.groupName = null; + this.systemContactPhoto = null; + this.customLabel = null; + this.contactUri = null; + this.signalProfileName = ProfileName.EMPTY; + this.profileAvatar = null; + this.hasProfileImage = false; + this.profileSharing = false; + this.lastProfileFetch = 0; + this.notificationChannel = null; + this.unidentifiedAccessMode = UnidentifiedAccessMode.DISABLED; + this.forceSmsSelection = false; + this.groupsV1MigrationCapability = Capability.UNKNOWN; + this.senderKeyCapability = Capability.UNKNOWN; + this.announcementGroupCapability = Capability.UNKNOWN; + this.changeNumberCapability = Capability.UNKNOWN; + this.storiesCapability = Capability.UNKNOWN; + this.giftBadgesCapability = Capability.UNKNOWN; + this.storageId = null; + this.mentionSetting = MentionSetting.ALWAYS_NOTIFY; + this.wallpaper = null; + this.chatColors = null; + this.avatarColor = AvatarColor.UNKNOWN; + this.about = null; + this.aboutEmoji = null; + this.systemProfileName = ProfileName.EMPTY; + this.systemContactName = null; + this.extras = Optional.empty(); + this.hasGroupsInCommon = false; + this.badges = Collections.emptyList(); + this.isReleaseNotesRecipient = false; } public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) { - this.id = id; - this.resolving = !resolved; - this.serviceId = details.serviceId; - this.pni = details.pni; - this.username = details.username; - this.e164 = details.e164; - this.email = details.email; - this.groupId = details.groupId; - this.distributionListId = details.distributionListId; - this.participants = details.participants; - this.groupAvatarId = details.groupAvatarId; - this.isSelf = details.isSelf; - this.blocked = details.blocked; - this.muteUntil = details.mutedUntil; - this.messageVibrate = details.messageVibrateState; - this.callVibrate = details.callVibrateState; - this.messageRingtone = details.messageRingtone; - this.callRingtone = details.callRingtone; - this.insightsBannerTier = details.insightsBannerTier; - this.defaultSubscriptionId = details.defaultSubscriptionId; - this.expireMessages = details.expireMessages; - this.registered = details.registered; - this.profileKey = details.profileKey; - this.profileKeyCredential = details.profileKeyCredential; - this.groupName = details.groupName; - this.systemContactPhoto = details.systemContactPhoto; - this.customLabel = details.customLabel; - this.contactUri = details.contactUri; - this.signalProfileName = details.profileName; - this.profileAvatar = details.profileAvatar; - this.hasProfileImage = details.hasProfileImage; - this.profileSharing = details.profileSharing; - this.lastProfileFetch = details.lastProfileFetch; - this.notificationChannel = details.notificationChannel; - this.unidentifiedAccessMode = details.unidentifiedAccessMode; - this.forceSmsSelection = details.forceSmsSelection; - this.groupsV1MigrationCapability = details.groupsV1MigrationCapability; - this.senderKeyCapability = details.senderKeyCapability; - this.announcementGroupCapability = details.announcementGroupCapability; - this.changeNumberCapability = details.changeNumberCapability; - this.storiesCapability = details.storiesCapability; - this.giftBadgesCapability = details.giftBadgesCapability; - this.storageId = details.storageId; - this.mentionSetting = details.mentionSetting; - this.wallpaper = details.wallpaper; - this.chatColors = details.chatColors; - this.avatarColor = details.avatarColor; - this.about = details.about; - this.aboutEmoji = details.aboutEmoji; - this.systemProfileName = details.systemProfileName; - this.systemContactName = details.systemContactName; - this.extras = details.extras; - this.hasGroupsInCommon = details.hasGroupsInCommon; - this.badges = details.badges; - this.isReleaseNotesRecipient = details.isReleaseChannel; + this.id = id; + this.resolving = !resolved; + this.serviceId = details.serviceId; + this.pni = details.pni; + this.username = details.username; + this.e164 = details.e164; + this.email = details.email; + this.groupId = details.groupId; + this.distributionListId = details.distributionListId; + this.participants = details.participants; + this.groupAvatarId = details.groupAvatarId; + this.isSelf = details.isSelf; + this.blocked = details.blocked; + this.muteUntil = details.mutedUntil; + this.messageVibrate = details.messageVibrateState; + this.callVibrate = details.callVibrateState; + this.messageRingtone = details.messageRingtone; + this.callRingtone = details.callRingtone; + this.insightsBannerTier = details.insightsBannerTier; + this.defaultSubscriptionId = details.defaultSubscriptionId; + this.expireMessages = details.expireMessages; + this.registered = details.registered; + this.profileKey = details.profileKey; + this.expiringProfileKeyCredential = details.expiringProfileKeyCredential; + this.groupName = details.groupName; + this.systemContactPhoto = details.systemContactPhoto; + this.customLabel = details.customLabel; + this.contactUri = details.contactUri; + this.signalProfileName = details.profileName; + this.profileAvatar = details.profileAvatar; + this.hasProfileImage = details.hasProfileImage; + this.profileSharing = details.profileSharing; + this.lastProfileFetch = details.lastProfileFetch; + this.notificationChannel = details.notificationChannel; + this.unidentifiedAccessMode = details.unidentifiedAccessMode; + this.forceSmsSelection = details.forceSmsSelection; + this.groupsV1MigrationCapability = details.groupsV1MigrationCapability; + this.senderKeyCapability = details.senderKeyCapability; + this.announcementGroupCapability = details.announcementGroupCapability; + this.changeNumberCapability = details.changeNumberCapability; + this.storiesCapability = details.storiesCapability; + this.giftBadgesCapability = details.giftBadgesCapability; + this.storageId = details.storageId; + this.mentionSetting = details.mentionSetting; + this.wallpaper = details.wallpaper; + this.chatColors = details.chatColors; + this.avatarColor = details.avatarColor; + this.about = details.about; + this.aboutEmoji = details.aboutEmoji; + this.systemProfileName = details.systemProfileName; + this.systemContactName = details.systemContactName; + this.extras = details.extras; + this.hasGroupsInCommon = details.hasGroupsInCommon; + this.badges = details.badges; + this.isReleaseNotesRecipient = details.isReleaseChannel; } public @NonNull RecipientId getId() { @@ -1022,12 +1022,8 @@ public class Recipient { return profileKey; } - public @Nullable ProfileKeyCredential getProfileKeyCredential() { - return profileKeyCredential; - } - - public boolean hasProfileKeyCredential() { - return profileKeyCredential != null; + public @Nullable ExpiringProfileKeyCredential getExpiringProfileKeyCredential() { + return expiringProfileKeyCredential; } public @Nullable byte[] getStorageServiceId() { @@ -1285,7 +1281,7 @@ public class Recipient { Objects.equals(defaultSubscriptionId, other.defaultSubscriptionId) && registered == other.registered && Arrays.equals(profileKey, other.profileKey) && - Objects.equals(profileKeyCredential, other.profileKeyCredential) && + Objects.equals(expiringProfileKeyCredential, other.expiringProfileKeyCredential) && Objects.equals(groupName, other.groupName) && Objects.equals(systemContactPhoto, other.systemContactPhoto) && Objects.equals(customLabel, other.customLabel) && diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java index 1a9e71634..fa317ec76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java @@ -6,7 +6,7 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.conversation.colors.AvatarColor; import org.thoughtcrime.securesms.conversation.colors.ChatColors; @@ -33,60 +33,60 @@ import java.util.Optional; public class RecipientDetails { - final ServiceId serviceId; - final PNI pni; - final String username; - final String e164; - final String email; - final GroupId groupId; - final DistributionListId distributionListId; - final String groupName; - final String systemContactName; - final String customLabel; - final Uri systemContactPhoto; - final Uri contactUri; - final Optional groupAvatarId; - final Uri messageRingtone; - final Uri callRingtone; - final long mutedUntil; - final VibrateState messageVibrateState; - final VibrateState callVibrateState; - final boolean blocked; - final int expireMessages; - final List participants; - final ProfileName profileName; - final Optional defaultSubscriptionId; - final RegisteredState registered; - final byte[] profileKey; - final ProfileKeyCredential profileKeyCredential; - final String profileAvatar; - final boolean hasProfileImage; - final boolean profileSharing; - final long lastProfileFetch; - final boolean systemContact; - final boolean isSelf; - final String notificationChannel; - final UnidentifiedAccessMode unidentifiedAccessMode; - final boolean forceSmsSelection; - final Recipient.Capability groupsV1MigrationCapability; - final Recipient.Capability senderKeyCapability; - final Recipient.Capability announcementGroupCapability; - final Recipient.Capability changeNumberCapability; - final Recipient.Capability storiesCapability; - final Recipient.Capability giftBadgesCapability; - final InsightsBannerTier insightsBannerTier; - final byte[] storageId; - final MentionSetting mentionSetting; - final ChatWallpaper wallpaper; - final ChatColors chatColors; - final AvatarColor avatarColor; - final String about; - final String aboutEmoji; - final ProfileName systemProfileName; - final Optional extras; - final boolean hasGroupsInCommon; - final List badges; - final boolean isReleaseChannel; + final ServiceId serviceId; + final PNI pni; + final String username; + final String e164; + final String email; + final GroupId groupId; + final DistributionListId distributionListId; + final String groupName; + final String systemContactName; + final String customLabel; + final Uri systemContactPhoto; + final Uri contactUri; + final Optional groupAvatarId; + final Uri messageRingtone; + final Uri callRingtone; + final long mutedUntil; + final VibrateState messageVibrateState; + final VibrateState callVibrateState; + final boolean blocked; + final int expireMessages; + final List participants; + final ProfileName profileName; + final Optional defaultSubscriptionId; + final RegisteredState registered; + final byte[] profileKey; + final ExpiringProfileKeyCredential expiringProfileKeyCredential; + final String profileAvatar; + final boolean hasProfileImage; + final boolean profileSharing; + final long lastProfileFetch; + final boolean systemContact; + final boolean isSelf; + final String notificationChannel; + final UnidentifiedAccessMode unidentifiedAccessMode; + final boolean forceSmsSelection; + final Recipient.Capability groupsV1MigrationCapability; + final Recipient.Capability senderKeyCapability; + final Recipient.Capability announcementGroupCapability; + final Recipient.Capability changeNumberCapability; + final Recipient.Capability storiesCapability; + final Recipient.Capability giftBadgesCapability; + final InsightsBannerTier insightsBannerTier; + final byte[] storageId; + final MentionSetting mentionSetting; + final ChatWallpaper wallpaper; + final ChatColors chatColors; + final AvatarColor avatarColor; + final String about; + final String aboutEmoji; + final ProfileName systemProfileName; + final Optional extras; + final boolean hasGroupsInCommon; + final List badges; + final boolean isReleaseChannel; public RecipientDetails(@Nullable String groupName, @Nullable String systemContactName, @@ -98,117 +98,117 @@ public class RecipientDetails { @Nullable List participants, boolean isReleaseChannel) { - this.groupAvatarId = groupAvatarId; - this.systemContactPhoto = Util.uri(record.getSystemContactPhotoUri()); - this.customLabel = record.getSystemPhoneLabel(); - this.contactUri = Util.uri(record.getSystemContactUri()); - this.serviceId = record.getServiceId(); - this.pni = record.getPni(); - this.username = record.getUsername(); - this.e164 = record.getE164(); - this.email = record.getEmail(); - this.groupId = record.getGroupId(); - this.distributionListId = record.getDistributionListId(); - this.messageRingtone = record.getMessageRingtone(); - this.callRingtone = record.getCallRingtone(); - this.mutedUntil = record.getMuteUntil(); - this.messageVibrateState = record.getMessageVibrateState(); - this.callVibrateState = record.getCallVibrateState(); - this.blocked = record.isBlocked(); - this.expireMessages = record.getExpireMessages(); - this.participants = participants == null ? new LinkedList<>() : participants; - this.profileName = record.getProfileName(); - this.defaultSubscriptionId = record.getDefaultSubscriptionId(); - this.registered = registeredState; - this.profileKey = record.getProfileKey(); - this.profileKeyCredential = record.getProfileKeyCredential(); - this.profileAvatar = record.getProfileAvatar(); - this.hasProfileImage = record.hasProfileImage(); - this.profileSharing = record.isProfileSharing(); - this.lastProfileFetch = record.getLastProfileFetch(); - this.systemContact = systemContact; - this.isSelf = isSelf; - this.notificationChannel = record.getNotificationChannel(); - this.unidentifiedAccessMode = record.getUnidentifiedAccessMode(); - this.forceSmsSelection = record.isForceSmsSelection(); - this.groupsV1MigrationCapability = record.getGroupsV1MigrationCapability(); - this.senderKeyCapability = record.getSenderKeyCapability(); - this.announcementGroupCapability = record.getAnnouncementGroupCapability(); - this.changeNumberCapability = record.getChangeNumberCapability(); - this.storiesCapability = record.getStoriesCapability(); - this.giftBadgesCapability = record.getGiftBadgesCapability(); - this.insightsBannerTier = record.getInsightsBannerTier(); - this.storageId = record.getStorageId(); - this.mentionSetting = record.getMentionSetting(); - this.wallpaper = record.getWallpaper(); - this.chatColors = record.getChatColors(); - this.avatarColor = record.getAvatarColor(); - this.about = record.getAbout(); - this.aboutEmoji = record.getAboutEmoji(); - this.systemProfileName = record.getSystemProfileName(); - this.groupName = groupName; - this.systemContactName = systemContactName; - this.extras = Optional.ofNullable(record.getExtras()); - this.hasGroupsInCommon = record.hasGroupsInCommon(); - this.badges = record.getBadges(); - this.isReleaseChannel = isReleaseChannel; + this.groupAvatarId = groupAvatarId; + this.systemContactPhoto = Util.uri(record.getSystemContactPhotoUri()); + this.customLabel = record.getSystemPhoneLabel(); + this.contactUri = Util.uri(record.getSystemContactUri()); + this.serviceId = record.getServiceId(); + this.pni = record.getPni(); + this.username = record.getUsername(); + this.e164 = record.getE164(); + this.email = record.getEmail(); + this.groupId = record.getGroupId(); + this.distributionListId = record.getDistributionListId(); + this.messageRingtone = record.getMessageRingtone(); + this.callRingtone = record.getCallRingtone(); + this.mutedUntil = record.getMuteUntil(); + this.messageVibrateState = record.getMessageVibrateState(); + this.callVibrateState = record.getCallVibrateState(); + this.blocked = record.isBlocked(); + this.expireMessages = record.getExpireMessages(); + this.participants = participants == null ? new LinkedList<>() : participants; + this.profileName = record.getProfileName(); + this.defaultSubscriptionId = record.getDefaultSubscriptionId(); + this.registered = registeredState; + this.profileKey = record.getProfileKey(); + this.expiringProfileKeyCredential = record.getExpiringProfileKeyCredential(); + this.profileAvatar = record.getProfileAvatar(); + this.hasProfileImage = record.hasProfileImage(); + this.profileSharing = record.isProfileSharing(); + this.lastProfileFetch = record.getLastProfileFetch(); + this.systemContact = systemContact; + this.isSelf = isSelf; + this.notificationChannel = record.getNotificationChannel(); + this.unidentifiedAccessMode = record.getUnidentifiedAccessMode(); + this.forceSmsSelection = record.isForceSmsSelection(); + this.groupsV1MigrationCapability = record.getGroupsV1MigrationCapability(); + this.senderKeyCapability = record.getSenderKeyCapability(); + this.announcementGroupCapability = record.getAnnouncementGroupCapability(); + this.changeNumberCapability = record.getChangeNumberCapability(); + this.storiesCapability = record.getStoriesCapability(); + this.giftBadgesCapability = record.getGiftBadgesCapability(); + this.insightsBannerTier = record.getInsightsBannerTier(); + this.storageId = record.getStorageId(); + this.mentionSetting = record.getMentionSetting(); + this.wallpaper = record.getWallpaper(); + this.chatColors = record.getChatColors(); + this.avatarColor = record.getAvatarColor(); + this.about = record.getAbout(); + this.aboutEmoji = record.getAboutEmoji(); + this.systemProfileName = record.getSystemProfileName(); + this.groupName = groupName; + this.systemContactName = systemContactName; + this.extras = Optional.ofNullable(record.getExtras()); + this.hasGroupsInCommon = record.hasGroupsInCommon(); + this.badges = record.getBadges(); + this.isReleaseChannel = isReleaseChannel; } private RecipientDetails() { - this.groupAvatarId = null; - this.systemContactPhoto = null; - this.customLabel = null; - this.contactUri = null; - this.serviceId = null; - this.pni = null; - this.username = null; - this.e164 = null; - this.email = null; - this.groupId = null; - this.distributionListId = null; - this.messageRingtone = null; - this.callRingtone = null; - this.mutedUntil = 0; - this.messageVibrateState = VibrateState.DEFAULT; - this.callVibrateState = VibrateState.DEFAULT; - this.blocked = false; - this.expireMessages = 0; - this.participants = new LinkedList<>(); - this.profileName = ProfileName.EMPTY; - this.insightsBannerTier = InsightsBannerTier.TIER_TWO; - this.defaultSubscriptionId = Optional.empty(); - this.registered = RegisteredState.UNKNOWN; - this.profileKey = null; - this.profileKeyCredential = null; - this.profileAvatar = null; - this.hasProfileImage = false; - this.profileSharing = false; - this.lastProfileFetch = 0; - this.systemContact = true; - this.isSelf = false; - this.notificationChannel = null; - this.unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN; - this.forceSmsSelection = false; - this.groupName = null; - this.groupsV1MigrationCapability = Recipient.Capability.UNKNOWN; - this.senderKeyCapability = Recipient.Capability.UNKNOWN; - this.announcementGroupCapability = Recipient.Capability.UNKNOWN; - this.changeNumberCapability = Recipient.Capability.UNKNOWN; - this.storiesCapability = Recipient.Capability.UNKNOWN; - this.giftBadgesCapability = Recipient.Capability.UNKNOWN; - this.storageId = null; - this.mentionSetting = MentionSetting.ALWAYS_NOTIFY; - this.wallpaper = null; - this.chatColors = null; - this.avatarColor = AvatarColor.UNKNOWN; - this.about = null; - this.aboutEmoji = null; - this.systemProfileName = ProfileName.EMPTY; - this.systemContactName = null; - this.extras = Optional.empty(); - this.hasGroupsInCommon = false; - this.badges = Collections.emptyList(); - this.isReleaseChannel = false; + this.groupAvatarId = null; + this.systemContactPhoto = null; + this.customLabel = null; + this.contactUri = null; + this.serviceId = null; + this.pni = null; + this.username = null; + this.e164 = null; + this.email = null; + this.groupId = null; + this.distributionListId = null; + this.messageRingtone = null; + this.callRingtone = null; + this.mutedUntil = 0; + this.messageVibrateState = VibrateState.DEFAULT; + this.callVibrateState = VibrateState.DEFAULT; + this.blocked = false; + this.expireMessages = 0; + this.participants = new LinkedList<>(); + this.profileName = ProfileName.EMPTY; + this.insightsBannerTier = InsightsBannerTier.TIER_TWO; + this.defaultSubscriptionId = Optional.empty(); + this.registered = RegisteredState.UNKNOWN; + this.profileKey = null; + this.expiringProfileKeyCredential = null; + this.profileAvatar = null; + this.hasProfileImage = false; + this.profileSharing = false; + this.lastProfileFetch = 0; + this.systemContact = true; + this.isSelf = false; + this.notificationChannel = null; + this.unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN; + this.forceSmsSelection = false; + this.groupName = null; + this.groupsV1MigrationCapability = Recipient.Capability.UNKNOWN; + this.senderKeyCapability = Recipient.Capability.UNKNOWN; + this.announcementGroupCapability = Recipient.Capability.UNKNOWN; + this.changeNumberCapability = Recipient.Capability.UNKNOWN; + this.storiesCapability = Recipient.Capability.UNKNOWN; + this.giftBadgesCapability = Recipient.Capability.UNKNOWN; + this.storageId = null; + this.mentionSetting = MentionSetting.ALWAYS_NOTIFY; + this.wallpaper = null; + this.chatColors = null; + this.avatarColor = AvatarColor.UNKNOWN; + this.about = null; + this.aboutEmoji = null; + this.systemProfileName = ProfileName.EMPTY; + this.systemContactName = null; + this.extras = Optional.empty(); + this.hasGroupsInCommon = false; + this.badges = Collections.emptyList(); + this.isReleaseChannel = false; } public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientRecord settings) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 4d93e33b1..8f4352698 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -85,7 +85,6 @@ public final class FeatureFlags { private static final String GROUP_CALL_RINGING = "android.calling.groupCallRinging"; private static final String DONOR_BADGES = "android.donorBadges.6"; private static final String DONOR_BADGES_DISPLAY = "android.donorBadges.display.4"; - private static final String CDSH = "android.cdsh"; private static final String STORIES = "android.stories.2"; private static final String STORIES_TEXT_FUNCTIONS = "android.stories.text.functions"; private static final String HARDWARE_AEC_BLOCKLIST_MODELS = "android.calling.hardwareAecBlockList"; @@ -134,7 +133,6 @@ public final class FeatureFlags { SUGGEST_SMS_BLACKLIST, MAX_GROUP_CALL_RING_SIZE, GROUP_CALL_RINGING, - CDSH, SENDER_KEY_MAX_AGE, DONOR_BADGES, DONOR_BADGES_DISPLAY, @@ -199,7 +197,6 @@ public final class FeatureFlags { SENDER_KEY, MAX_GROUP_CALL_RING_SIZE, GROUP_CALL_RINGING, - CDSH, SENDER_KEY_MAX_AGE, DONOR_BADGES_DISPLAY, DONATE_MEGAPHONE, @@ -470,10 +467,6 @@ public final class FeatureFlags { return getBoolean(DONOR_BADGES_DISPLAY, true); } - public static boolean cdsh() { - return Environment.IS_STAGING && getBoolean(CDSH, false); - } - /** A comma-separated list of models that should *not* use hardware AEC for calling. */ public static @NonNull String hardwareAecBlocklistModels() { return getString(HARDWARE_AEC_BLOCKLIST_MODELS, ""); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/JavaTimeExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/util/JavaTimeExtensions.kt index 1e0e3e2a9..ceda89be0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/JavaTimeExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/JavaTimeExtensions.kt @@ -44,6 +44,13 @@ fun Long.toLocalDateTime(zoneId: ZoneId = ZoneId.systemDefault()): LocalDateTime return LocalDateTime.ofInstant(Instant.ofEpochMilli(this), zoneId) } +/** + * Convert milliseconds to local date time with provided [zoneId]. + */ +fun Instant.toLocalDateTime(zoneId: ZoneId = ZoneId.systemDefault()): LocalDateTime { + return LocalDateTime.ofInstant(this, zoneId) +} + /** * Converts milliseconds to local time with provided [zoneId]. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java index e6fb8ea93..fce249089 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java @@ -12,6 +12,7 @@ import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.util.Pair; import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; @@ -282,6 +283,35 @@ public final class ProfileUtil { Recipient.self().getBadges()); } + /** + * Attempts to update just the expiring profile key credential with a new one. If unable, an empty optional is returned. + * + * Note: It will try to find missing profile key credentials from the server and persist locally. + */ + public static Optional updateExpiringProfileKeyCredential(@NonNull Recipient recipient) throws IOException { + ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey()); + + if (profileKey != null) { + Log.i(TAG, String.format("Updating profile key credential on recipient %s, fetching", recipient.getId())); + + Optional profileKeyCredentialOptional = ApplicationDependencies.getSignalServiceAccountManager() + .resolveProfileKeyCredential(recipient.requireServiceId(), profileKey, Locale.getDefault()); + + if (profileKeyCredentialOptional.isPresent()) { + boolean updatedProfileKey = SignalDatabase.recipients().setProfileKeyCredential(recipient.getId(), profileKey, profileKeyCredentialOptional.get()); + + if (!updatedProfileKey) { + Log.w(TAG, String.format("Failed to update the profile key credential on recipient %s", recipient.getId())); + } else { + Log.i(TAG, String.format("Got new profile key credential for recipient %s", recipient.getId())); + return profileKeyCredentialOptional; + } + } + } + + return Optional.empty(); + } + private static void uploadProfile(@NonNull ProfileName profileName, @Nullable String about, @Nullable String aboutEmoji, diff --git a/app/src/main/proto/Database.proto b/app/src/main/proto/Database.proto index 352f9df82..716010f6e 100644 --- a/app/src/main/proto/Database.proto +++ b/app/src/main/proto/Database.proto @@ -52,7 +52,7 @@ message DecryptedGroupV2Context { } message TemporalAuthCredentialResponse { - int32 date = 1; + int64 date = 1; bytes authCredentialResponse = 2; } @@ -118,9 +118,9 @@ message GroupCallUpdateDetails { bool isCallFull = 5; } -message ProfileKeyCredentialColumnData { +message ExpiringProfileKeyCredentialColumnData { bytes profileKey = 1; - bytes profileKeyCredential = 2; + bytes expiringProfileKeyCredential = 2; } message DeviceLastResetTime { diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt b/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt index 2989aa8da..519de9448 100644 --- a/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt +++ b/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt @@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.database.LocalMetricsDatabase import org.thoughtcrime.securesms.database.LogDatabase import org.thoughtcrime.securesms.database.MegaphoneDatabase import org.thoughtcrime.securesms.database.MessageBitmaskColumnTransformer +import org.thoughtcrime.securesms.database.ProfileKeyCredentialTransformer import org.thoughtcrime.securesms.database.QueryMonitor import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.TimestampTransformer @@ -62,7 +63,7 @@ class SpinnerApplicationContext : ApplicationContext() { linkedMapOf( "signal" to DatabaseConfig( db = SignalDatabase.rawDatabase, - columnTransformers = listOf(MessageBitmaskColumnTransformer, GV2Transformer, GV2UpdateTransformer, IsStoryTransformer, TimestampTransformer) + columnTransformers = listOf(MessageBitmaskColumnTransformer, GV2Transformer, GV2UpdateTransformer, IsStoryTransformer, TimestampTransformer, ProfileKeyCredentialTransformer) ), "jobmanager" to DatabaseConfig(db = JobDatabase.getInstance(this).sqlCipherDatabase), "keyvalue" to DatabaseConfig(db = KeyValueDatabase.getInstance(this).sqlCipherDatabase), diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/database/GV2UpdateTransformer.kt b/app/src/spinner/java/org/thoughtcrime/securesms/database/GV2UpdateTransformer.kt index 9312b0aef..dd9a2c2d7 100644 --- a/app/src/spinner/java/org/thoughtcrime/securesms/database/GV2UpdateTransformer.kt +++ b/app/src/spinner/java/org/thoughtcrime/securesms/database/GV2UpdateTransformer.kt @@ -7,7 +7,9 @@ import org.signal.spinner.ColumnTransformer import org.signal.spinner.DefaultColumnTransformer import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.UpdateDescription +import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.util.Base64 object GV2UpdateTransformer : ColumnTransformer { override fun matches(tableName: String?, columnName: String): Boolean { @@ -24,8 +26,11 @@ object GV2UpdateTransformer : ColumnTransformer { val body: String? = CursorUtil.requireString(cursor, MmsSmsColumns.BODY) return if (MmsSmsColumns.Types.isGroupV2(type) && MmsSmsColumns.Types.isGroupUpdate(type) && body != null) { + val decoded = Base64.decode(body) + val decryptedGroupV2Context = DecryptedGroupV2Context.parseFrom(decoded) val gv2ChangeDescription: UpdateDescription = MessageRecord.getGv2ChangeDescription(ApplicationDependencies.getApplication(), body, null) - gv2ChangeDescription.spannable.toString() + + "${gv2ChangeDescription.spannable}

${decryptedGroupV2Context.change}" } else { body ?: "" } diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/database/ProfileKeyCredentialTransformer.kt b/app/src/spinner/java/org/thoughtcrime/securesms/database/ProfileKeyCredentialTransformer.kt new file mode 100644 index 000000000..6db4ca3fb --- /dev/null +++ b/app/src/spinner/java/org/thoughtcrime/securesms/database/ProfileKeyCredentialTransformer.kt @@ -0,0 +1,33 @@ +package org.thoughtcrime.securesms.database + +import android.database.Cursor +import org.signal.core.util.Hex +import org.signal.core.util.requireString +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential +import org.signal.spinner.ColumnTransformer +import org.signal.spinner.DefaultColumnTransformer +import org.thoughtcrime.securesms.database.model.databaseprotos.ExpiringProfileKeyCredentialColumnData +import org.thoughtcrime.securesms.util.Base64 +import org.thoughtcrime.securesms.util.toLocalDateTime +import java.security.MessageDigest + +object ProfileKeyCredentialTransformer : ColumnTransformer { + override fun matches(tableName: String?, columnName: String): Boolean { + return columnName == RecipientDatabase.EXPIRING_PROFILE_KEY_CREDENTIAL && (tableName == null || tableName == RecipientDatabase.TABLE_NAME) + } + + override fun transform(tableName: String?, columnName: String, cursor: Cursor): String { + val columnDataString = cursor.requireString(RecipientDatabase.EXPIRING_PROFILE_KEY_CREDENTIAL) ?: return DefaultColumnTransformer.transform(tableName, columnName, cursor) + val columnDataBytes = Base64.decode(columnDataString) + val columnData = ExpiringProfileKeyCredentialColumnData.parseFrom(columnDataBytes) + val credential = ExpiringProfileKeyCredential(columnData.expiringProfileKeyCredential.toByteArray()) + + return """ + Credential: ${Hex.toStringCondensed(MessageDigest.getInstance("SHA-256").digest(credential.serialize()))} + Expires: ${credential.expirationTime.toLocalDateTime()} + + Matching Profile Key: + ${Base64.encodeBytes(columnData.profileKey.toByteArray())} + """.trimIndent().replace("\n", "
") + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt b/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt index 7431955f9..83c84a711 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt @@ -21,7 +21,6 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations import org.whispersystems.signalservice.api.push.DistributionId import org.whispersystems.signalservice.api.push.ServiceId import java.util.Optional -import java.util.UUID fun DecryptedGroupChange.Builder.setNewDescription(description: String) { newDescription = DecryptedString.newBuilder().setValue(description).build() @@ -118,11 +117,10 @@ class GroupStateTestData(private val masterKey: GroupMasterKey, private val grou pendingMembers: List = emptyList(), requestingMembers: List = emptyList(), inviteLinkPassword: ByteArray = ByteArray(0), - disappearingMessageTimer: DecryptedTimer = DecryptedTimer.getDefaultInstance(), - serviceId: String = ServiceId.from(UUID.randomUUID()).toString() + disappearingMessageTimer: DecryptedTimer = DecryptedTimer.getDefaultInstance() ) { localState = decryptedGroup(revision, title, avatar, description, accessControl, members, pendingMembers, requestingMembers, inviteLinkPassword, disappearingMessageTimer) - groupRecord = groupRecord(masterKey, localState!!, active = active, serviceId = serviceId) + groupRecord = groupRecord(masterKey, localState!!, active = active) } fun serverState( @@ -173,8 +171,7 @@ fun groupRecord( active: Boolean = true, avatarDigest: ByteArray = ByteArray(0), mms: Boolean = false, - distributionId: DistributionId? = null, - serviceId: String = ServiceId.from(UUID.randomUUID()).toString() + distributionId: DistributionId? = null ): Optional { return Optional.of( GroupDatabase.GroupRecord( @@ -193,8 +190,7 @@ fun groupRecord( masterKey.serialize(), decryptedGroup.revision, decryptedGroup.toByteArray(), - distributionId, - serviceId + distributionId ) ) } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt index 0167c1b30..48f4a1481 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt @@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database import android.net.Uri import org.signal.core.util.Bitmask -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.conversation.colors.ChatColors @@ -47,7 +47,7 @@ object RecipientDatabaseTestUtils { expireMessages: Int = 0, registered: RecipientDatabase.RegisteredState = RecipientDatabase.RegisteredState.REGISTERED, profileKey: ByteArray = Random.nextBytes(32), - profileKeyCredential: ProfileKeyCredential? = null, + expiringProfileKeyCredential: ExpiringProfileKeyCredential? = null, systemProfileName: ProfileName = ProfileName.EMPTY, systemDisplayName: String? = null, systemContactPhoto: String? = null, @@ -111,7 +111,7 @@ object RecipientDatabaseTestUtils { expireMessages, registered, profileKey, - profileKeyCredential, + expiringProfileKeyCredential, systemProfileName, systemDisplayName, systemContactPhoto, diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java index e103bce4e..68b6ac0a7 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java @@ -24,12 +24,12 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; -import org.thoughtcrime.securesms.keyvalue.ServiceIds; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.ServiceId; +import org.whispersystems.signalservice.api.push.ServiceIds; import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.Arrays; diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/GroupManagerV2Test_edit.kt b/app/src/test/java/org/thoughtcrime/securesms/groups/GroupManagerV2Test_edit.kt index ff855c757..2424a776a 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/GroupManagerV2Test_edit.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/GroupManagerV2Test_edit.kt @@ -41,6 +41,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations import org.whispersystems.signalservice.api.push.ACI import org.whispersystems.signalservice.api.push.PNI import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.api.push.ServiceIds import java.util.UUID @RunWith(RobolectricTestRunner::class) @@ -55,6 +56,7 @@ class GroupManagerV2Test_edit { val selfAci: ACI = ACI.from(UUID.randomUUID()) val selfPni: PNI = PNI.from(UUID.randomUUID()) + val serviceIds: ServiceIds = ServiceIds(selfAci, selfPni) val otherSid: ServiceId = ServiceId.from(UUID.randomUUID()) val selfAndOthers: List = listOf(member(selfAci), member(otherSid)) val others: List = listOf(member(otherSid)) @@ -103,8 +105,7 @@ class GroupManagerV2Test_edit { groupsV2Operations, groupsV2Authorization, groupsV2StateProcessor, - selfAci, - selfPni, + serviceIds, groupCandidateHelper, sendGroupUpdateHelper ) @@ -139,8 +140,7 @@ class GroupManagerV2Test_edit { members = listOf( member(selfAci, role = Member.Role.ADMINISTRATOR), member(otherSid) - ), - serviceId = selfAci.toString() + ) ) groupChange(6) { source(selfAci) diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt index 06cd39f62..deca098ce 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt @@ -43,7 +43,9 @@ import org.thoughtcrime.securesms.testutil.SystemOutLogger import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api import org.whispersystems.signalservice.api.groupsv2.PartialDecryptedGroup import org.whispersystems.signalservice.api.push.ACI +import org.whispersystems.signalservice.api.push.PNI import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.api.push.ServiceIds import java.util.UUID @RunWith(RobolectricTestRunner::class) @@ -53,6 +55,7 @@ class GroupsV2StateProcessorTest { companion object { private val masterKey = GroupMasterKey(fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) private val selfAci: ACI = ACI.from(UUID.randomUUID()) + private val serviceIds: ServiceIds = ServiceIds(selfAci, PNI.from(UUID.randomUUID())) private val otherSid: ServiceId = ServiceId.from(UUID.randomUUID()) private val selfAndOthers: List = listOf(member(selfAci), member(otherSid)) private val others: List = listOf(member(otherSid)) @@ -80,7 +83,7 @@ class GroupsV2StateProcessorTest { groupsV2Authorization = mock(GroupsV2Authorization::class.java) profileAndMessageHelper = mock(GroupsV2StateProcessor.ProfileAndMessageHelper::class.java) - processor = GroupsV2StateProcessor.StateProcessorForGroup(selfAci, ApplicationProvider.getApplicationContext(), groupDatabase, groupsV2API, groupsV2Authorization, masterKey, profileAndMessageHelper) + processor = GroupsV2StateProcessor.StateProcessorForGroup(serviceIds, ApplicationProvider.getApplicationContext(), groupDatabase, groupsV2API, groupsV2Authorization, masterKey, profileAndMessageHelper) } @After diff --git a/build.gradle b/build.gradle index 349166a21..e17857585 100644 --- a/build.gradle +++ b/build.gradle @@ -35,8 +35,6 @@ ext { MINIMUM_SDK = 19 JAVA_VERSION = JavaVersion.VERSION_1_8 - - LIBSIGNAL_CLIENT_VERSION = '0.9.4' } wrapper { diff --git a/dependencies.gradle b/dependencies.gradle index 94f457604..9dcd98aa5 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -4,7 +4,7 @@ dependencyResolutionManagement { versionCatalogs { libs { - version('libsignal-client', '0.17.0') + version('libsignal-client', '0.18.1') version('exoplayer', '2.15.0') version('androidx-camera', '1.0.0-beta11') version('androidx-lifecycle', '2.3.1') diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index dd9b58647..3f606dc20 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -5508,20 +5508,20 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - + + - - - + + + - - + + diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index c607b58b5..f73ff2c98 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -16,8 +16,8 @@ import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.logging.Log; import org.signal.libsignal.protocol.state.PreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.ProfileCipher; @@ -824,12 +824,12 @@ public class SignalServiceAccountManager { profileAvatarData); } - public Optional resolveProfileKeyCredential(ServiceId serviceId, ProfileKey profileKey, Locale locale) + public Optional resolveProfileKeyCredential(ServiceId serviceId, ProfileKey profileKey, Locale locale) throws NonSuccessfulResponseCodeException, PushNetworkException { try { ProfileAndCredential credential = this.pushServiceSocket.retrieveVersionedProfileAndCredential(serviceId.uuid(), profileKey, Optional.empty(), locale).get(10, TimeUnit.SECONDS); - return credential.getProfileKeyCredential(); + return credential.getExpiringProfileKeyCredential(); } catch (InterruptedException | TimeoutException e) { throw new PushNetworkException(e); } catch (ExecutionException e) { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ChangeSetModifier.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ChangeSetModifier.java index 1f01ea975..ec05ad584 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ChangeSetModifier.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ChangeSetModifier.java @@ -44,4 +44,6 @@ public interface ChangeSetModifier { void removeAddBannedMembers(int i); void removeDeleteBannedMembers(int i); + + void removePromotePendingPniAciMembers(int i); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupChangeActionsBuilderChangeSetModifier.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupChangeActionsBuilderChangeSetModifier.java index 0151c7b38..a0f7dcadb 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupChangeActionsBuilderChangeSetModifier.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupChangeActionsBuilderChangeSetModifier.java @@ -143,6 +143,11 @@ final class DecryptedGroupChangeActionsBuilderChangeSetModifier implements Chang result.removeDeleteBannedMembers(i); } + @Override + public void removePromotePendingPniAciMembers(int i) { + result.removePromotePendingPniAciMembers(i); + } + private static List removeIndexFromByteStringList(List byteStrings, int i) { List modifiedList = new ArrayList<>(byteStrings); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java index 2ff49efcd..bbd879a09 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java @@ -15,6 +15,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval; import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember; import org.signal.storageservice.protos.groups.local.EnabledState; +import org.whispersystems.signalservice.api.push.ServiceIds; import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.ArrayList; @@ -193,6 +194,16 @@ public final class DecryptedGroupUtil { return Optional.empty(); } + public static Optional findPendingByServiceIds(Collection members, ServiceIds serviceIds) { + for (DecryptedPendingMember member : members) { + if (serviceIds.matches(member.getUuid())) { + return Optional.of(member); + } + } + + return Optional.empty(); + } + private static int findPendingIndexByUuidCipherText(List members, ByteString cipherText) { for (int i = 0; i < members.size(); i++) { DecryptedPendingMember member = members.get(i); @@ -227,9 +238,19 @@ public final class DecryptedGroupUtil { return Optional.empty(); } - public static boolean isPendingOrRequesting(DecryptedGroup group, UUID uuid) { - return findPendingByUuid(group.getPendingMembersList(), uuid).isPresent() || - findRequestingByUuid(group.getRequestingMembersList(), uuid).isPresent(); + public static Optional findRequestingByServiceIds(Collection members, ServiceIds serviceIds) { + for (DecryptedRequestingMember member : members) { + if (serviceIds.matches(member.getUuid())) { + return Optional.of(member); + } + } + + return Optional.empty(); + } + + public static boolean isPendingOrRequesting(DecryptedGroup group, ServiceIds serviceIds) { + return findPendingByServiceIds(group.getPendingMembersList(), serviceIds).isPresent() || + findRequestingByServiceIds(group.getRequestingMembersList(), serviceIds).isPresent(); } public static boolean isRequesting(DecryptedGroup group, UUID uuid) { @@ -324,6 +345,8 @@ public final class DecryptedGroupUtil { applyDeleteBannedMembersActions(builder, change.getDeleteBannedMembersList()); + applyPromotePendingPniAciMemberActions(builder, change.getPromotePendingPniAciMembersList()); + return builder.build(); } @@ -557,6 +580,19 @@ public final class DecryptedGroupUtil { } } + protected static void applyPromotePendingPniAciMemberActions(DecryptedGroup.Builder builder, List promotePendingPniAciMembersList) throws NotAbleToApplyGroupV2ChangeException { + for (DecryptedMember newMember : promotePendingPniAciMembersList) { + int index = findPendingIndexByUuid(builder.getPendingMembersList(), newMember.getPni()); + + if (index == -1) { + throw new NotAbleToApplyGroupV2ChangeException(); + } + + builder.removePendingMembers(index); + builder.addMembers(newMember); + } + } + private static DecryptedMember withNewProfileKey(DecryptedMember member, ByteString profileKey) { return DecryptedMember.newBuilder(member) .setProfileKey(profileKey) @@ -653,48 +689,50 @@ public final class DecryptedGroupUtil { * When updating this, update {@link #changeIsEmptyExceptForBanChangesAndOptionalProfileKeyChanges(DecryptedGroupChange)} */ public static boolean changeIsEmptyExceptForProfileKeyChanges(DecryptedGroupChange change) { - return change.getNewMembersCount() == 0 && // field 3 - change.getDeleteMembersCount() == 0 && // field 4 - change.getModifyMemberRolesCount() == 0 && // field 5 - change.getNewPendingMembersCount() == 0 && // field 7 - change.getDeletePendingMembersCount() == 0 && // field 8 - change.getPromotePendingMembersCount() == 0 && // field 9 - !change.hasNewTitle() && // field 10 - !change.hasNewAvatar() && // field 11 - !change.hasNewTimer() && // field 12 - isEmpty(change.getNewAttributeAccess()) && // field 13 - isEmpty(change.getNewMemberAccess()) && // field 14 - isEmpty(change.getNewInviteLinkAccess()) && // field 15 - change.getNewRequestingMembersCount() == 0 && // field 16 - change.getDeleteRequestingMembersCount() == 0 && // field 17 - change.getPromoteRequestingMembersCount() == 0 && // field 18 - change.getNewInviteLinkPassword().size() == 0 && // field 19 - !change.hasNewDescription() && // field 20 - isEmpty(change.getNewIsAnnouncementGroup()) && // field 21 - change.getNewBannedMembersCount() == 0 && // field 22 - change.getDeleteBannedMembersCount() == 0; // field 23 + return change.getNewMembersCount() == 0 && // field 3 + change.getDeleteMembersCount() == 0 && // field 4 + change.getModifyMemberRolesCount() == 0 && // field 5 + change.getNewPendingMembersCount() == 0 && // field 7 + change.getDeletePendingMembersCount() == 0 && // field 8 + change.getPromotePendingMembersCount() == 0 && // field 9 + !change.hasNewTitle() && // field 10 + !change.hasNewAvatar() && // field 11 + !change.hasNewTimer() && // field 12 + isEmpty(change.getNewAttributeAccess()) && // field 13 + isEmpty(change.getNewMemberAccess()) && // field 14 + isEmpty(change.getNewInviteLinkAccess()) && // field 15 + change.getNewRequestingMembersCount() == 0 && // field 16 + change.getDeleteRequestingMembersCount() == 0 && // field 17 + change.getPromoteRequestingMembersCount() == 0 && // field 18 + change.getNewInviteLinkPassword().size() == 0 && // field 19 + !change.hasNewDescription() && // field 20 + isEmpty(change.getNewIsAnnouncementGroup()) && // field 21 + change.getNewBannedMembersCount() == 0 && // field 22 + change.getDeleteBannedMembersCount() == 0 && // field 23 + change.getPromotePendingPniAciMembersCount() == 0; // field 24 } public static boolean changeIsEmptyExceptForBanChangesAndOptionalProfileKeyChanges(DecryptedGroupChange change) { return (change.getNewBannedMembersCount() != 0 || change.getDeleteBannedMembersCount() != 0) && - change.getNewMembersCount() == 0 && // field 3 - change.getDeleteMembersCount() == 0 && // field 4 - change.getModifyMemberRolesCount() == 0 && // field 5 - change.getNewPendingMembersCount() == 0 && // field 7 - change.getDeletePendingMembersCount() == 0 && // field 8 - change.getPromotePendingMembersCount() == 0 && // field 9 - !change.hasNewTitle() && // field 10 - !change.hasNewAvatar() && // field 11 - !change.hasNewTimer() && // field 12 - isEmpty(change.getNewAttributeAccess()) && // field 13 - isEmpty(change.getNewMemberAccess()) && // field 14 - isEmpty(change.getNewInviteLinkAccess()) && // field 15 - change.getNewRequestingMembersCount() == 0 && // field 16 - change.getDeleteRequestingMembersCount() == 0 && // field 17 - change.getPromoteRequestingMembersCount() == 0 && // field 18 - change.getNewInviteLinkPassword().size() == 0 && // field 19 - !change.hasNewDescription() && // field 20 - isEmpty(change.getNewIsAnnouncementGroup()); // field 21 + change.getNewMembersCount() == 0 && // field 3 + change.getDeleteMembersCount() == 0 && // field 4 + change.getModifyMemberRolesCount() == 0 && // field 5 + change.getNewPendingMembersCount() == 0 && // field 7 + change.getDeletePendingMembersCount() == 0 && // field 8 + change.getPromotePendingMembersCount() == 0 && // field 9 + !change.hasNewTitle() && // field 10 + !change.hasNewAvatar() && // field 11 + !change.hasNewTimer() && // field 12 + isEmpty(change.getNewAttributeAccess()) && // field 13 + isEmpty(change.getNewMemberAccess()) && // field 14 + isEmpty(change.getNewInviteLinkAccess()) && // field 15 + change.getNewRequestingMembersCount() == 0 && // field 16 + change.getDeleteRequestingMembersCount() == 0 && // field 17 + change.getPromoteRequestingMembersCount() == 0 && // field 18 + change.getNewInviteLinkPassword().size() == 0 && // field 19 + !change.hasNewDescription() && // field 20 + isEmpty(change.getNewIsAnnouncementGroup()) && // field 21 + change.getPromotePendingPniAciMembersCount() == 0; // field 24 } static boolean isEmpty(AccessControl.AccessRequired newAttributeAccess) { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupCandidate.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupCandidate.java index 9510ecef5..f326f81fc 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupCandidate.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupCandidate.java @@ -1,12 +1,9 @@ package org.whispersystems.signalservice.api.groupsv2; -import org.signal.libsignal.zkgroup.profiles.ProfileKey; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; +import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil; -import java.util.ArrayList; -import java.util.Collection; import java.util.HashSet; -import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -14,7 +11,7 @@ import java.util.UUID; /** * Represents a potential new member of a group. *

- * The entry may or may not have a {@link ProfileKeyCredential}. + * The entry may or may not have a {@link ExpiringProfileKeyCredential}. *

* If it does not, then this user can only be invited. *

@@ -22,60 +19,50 @@ import java.util.UUID; */ public final class GroupCandidate { - private final UUID uuid; - private final Optional profileKeyCredential; + private final UUID uuid; + private final Optional expiringProfileKeyCredential; - public GroupCandidate(UUID uuid, Optional profileKeyCredential) { - this.uuid = uuid; - this.profileKeyCredential = profileKeyCredential; + public GroupCandidate(UUID uuid, Optional expiringProfileKeyCredential) { + this.uuid = uuid; + this.expiringProfileKeyCredential = expiringProfileKeyCredential; } public UUID getUuid() { return uuid; } - public Optional getProfileKeyCredential() { - return profileKeyCredential; + public Optional getExpiringProfileKeyCredential() { + return expiringProfileKeyCredential; } - public ProfileKeyCredential requireProfileKeyCredential() { - if (profileKeyCredential.isPresent()) { - return profileKeyCredential.get(); + public ExpiringProfileKeyCredential requireExpiringProfileKeyCredential() { + if (expiringProfileKeyCredential.isPresent()) { + return expiringProfileKeyCredential.get(); } throw new IllegalStateException("no profile key credential"); } - public boolean hasProfileKeyCredential() { - return profileKeyCredential.isPresent(); + public boolean hasValidProfileKeyCredential() { + return expiringProfileKeyCredential.map(ExpiringProfileCredentialUtil::isValid).orElse(false); } - public static Set withoutProfileKeyCredentials(Set groupCandidates) { + public static Set withoutExpiringProfileKeyCredentials(Set groupCandidates) { HashSet result = new HashSet<>(groupCandidates.size()); - for (GroupCandidate candidate: groupCandidates) { - result.add(candidate.withoutProfileKeyCredential()); + for (GroupCandidate candidate : groupCandidates) { + result.add(candidate.withoutExpiringProfileKeyCredential()); } return result; } - public GroupCandidate withoutProfileKeyCredential() { - return hasProfileKeyCredential() ? new GroupCandidate(uuid, Optional.empty()) - : this; + public GroupCandidate withoutExpiringProfileKeyCredential() { + return expiringProfileKeyCredential.isPresent() ? new GroupCandidate(uuid, Optional.empty()) + : this; } - public GroupCandidate withProfileKeyCredential(ProfileKeyCredential profileKeyCredential) { - return new GroupCandidate(uuid, Optional.of(profileKeyCredential)); - } - - public static List toUuidList(Collection candidates) { - final List uuidList = new ArrayList<>(candidates.size()); - - for (GroupCandidate candidate : candidates) { - uuidList.add(candidate.getUuid()); - } - - return uuidList; + public GroupCandidate withExpiringProfileKeyCredential(ExpiringProfileKeyCredential expiringProfileKeyCredential) { + return new GroupCandidate(uuid, Optional.of(expiringProfileKeyCredential)); } @Override diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeActionsBuilderChangeSetModifier.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeActionsBuilderChangeSetModifier.java index f94ff3fea..17bb669a7 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeActionsBuilderChangeSetModifier.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeActionsBuilderChangeSetModifier.java @@ -122,4 +122,9 @@ final class GroupChangeActionsBuilderChangeSetModifier implements ChangeSetModif public void removeDeleteBannedMembers(int i) { result.removeDeleteBannedMembers(i); } + + @Override + public void removePromotePendingPniAciMembers(int i) { + result.removePromotePendingPniAciMembers(i); + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil.java index 7aa987d0a..814a957a0 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil.java @@ -25,27 +25,28 @@ public final class GroupChangeUtil { * True iff there are no change actions. */ public static boolean changeIsEmpty(GroupChange.Actions change) { - return change.getAddMembersCount() == 0 && // field 3 - change.getDeleteMembersCount() == 0 && // field 4 - change.getModifyMemberRolesCount() == 0 && // field 5 - change.getModifyMemberProfileKeysCount() == 0 && // field 6 - change.getAddPendingMembersCount() == 0 && // field 7 - change.getDeletePendingMembersCount() == 0 && // field 8 - change.getPromotePendingMembersCount() == 0 && // field 9 - !change.hasModifyTitle() && // field 10 - !change.hasModifyAvatar() && // field 11 - !change.hasModifyDisappearingMessagesTimer() && // field 12 - !change.hasModifyAttributesAccess() && // field 13 - !change.hasModifyMemberAccess() && // field 14 - !change.hasModifyAddFromInviteLinkAccess() && // field 15 - change.getAddRequestingMembersCount() == 0 && // field 16 - change.getDeleteRequestingMembersCount() == 0 && // field 17 - change.getPromoteRequestingMembersCount() == 0 && // field 18 - !change.hasModifyInviteLinkPassword() && // field 19 - !change.hasModifyDescription() && // field 20 - !change.hasModifyAnnouncementsOnly() && // field 21 - change.getAddBannedMembersCount() == 0 && // field 22 - change.getDeleteBannedMembersCount() == 0; // field 23 + return change.getAddMembersCount() == 0 && // field 3 + change.getDeleteMembersCount() == 0 && // field 4 + change.getModifyMemberRolesCount() == 0 && // field 5 + change.getModifyMemberProfileKeysCount() == 0 && // field 6 + change.getAddPendingMembersCount() == 0 && // field 7 + change.getDeletePendingMembersCount() == 0 && // field 8 + change.getPromotePendingMembersCount() == 0 && // field 9 + !change.hasModifyTitle() && // field 10 + !change.hasModifyAvatar() && // field 11 + !change.hasModifyDisappearingMessagesTimer() && // field 12 + !change.hasModifyAttributesAccess() && // field 13 + !change.hasModifyMemberAccess() && // field 14 + !change.hasModifyAddFromInviteLinkAccess() && // field 15 + change.getAddRequestingMembersCount() == 0 && // field 16 + change.getDeleteRequestingMembersCount() == 0 && // field 17 + change.getPromoteRequestingMembersCount() == 0 && // field 18 + !change.hasModifyInviteLinkPassword() && // field 19 + !change.hasModifyDescription() && // field 20 + !change.hasModifyAnnouncementsOnly() && // field 21 + change.getAddBannedMembersCount() == 0 && // field 22 + change.getDeleteBannedMembersCount() == 0 && // field 23 + change.getPromotePendingPniAciMembersCount() == 0; // field 24 } /** @@ -147,6 +148,7 @@ public final class GroupChangeUtil { resolveField21ModifyAnnouncementsOnly (groupState, conflictingChange, changeSetModifier); resolveField22AddBannedMembers (conflictingChange, changeSetModifier, bannedMembersByUuid); resolveField23DeleteBannedMembers (conflictingChange, changeSetModifier, bannedMembersByUuid); + resolveField24PromotePendingPniAciMembers (conflictingChange, changeSetModifier, fullMembersByUuid); } private static void resolveField3AddMembers(DecryptedGroupChange conflictingChange, ChangeSetModifier result, HashMap fullMembersByUuid, HashMap pendingMembersByUuid) { @@ -352,4 +354,16 @@ public final class GroupChangeUtil { } } } + + private static void resolveField24PromotePendingPniAciMembers(DecryptedGroupChange conflictingChange, ChangeSetModifier result, HashMap fullMembersByUuid) { + List promotePendingPniAciMembersList = conflictingChange.getPromotePendingPniAciMembersList(); + + for (int i = promotePendingPniAciMembersList.size() - 1; i >= 0; i--) { + DecryptedMember member = promotePendingPniAciMembersList.get(i); + + if (fullMembersByUuid.containsKey(member.getUuid())) { + result.removePromotePendingPniAciMembers(i); + } + } + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java index 0ffa9cc19..a800b68d3 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java @@ -7,6 +7,8 @@ import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.auth.AuthCredential; import org.signal.libsignal.zkgroup.auth.AuthCredentialPresentation; import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse; +import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPni; +import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse; import org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations; import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher; import org.signal.libsignal.zkgroup.groups.GroupSecretParams; @@ -44,24 +46,25 @@ public class GroupsV2Api { /** * Provides 7 days of credentials, which you should cache. */ - public HashMap getCredentials(int today, boolean isAci) + public HashMap getCredentials(long todaySeconds) throws IOException { - return parseCredentialResponse(socket.retrieveGroupsV2Credentials(today, isAci)); + return parseCredentialResponse(socket.retrieveGroupsV2Credentials(todaySeconds)); } /** * Create an auth token from a credential response. */ - public GroupsV2AuthorizationString getGroupsV2AuthorizationString(ServiceId self, - int today, + public GroupsV2AuthorizationString getGroupsV2AuthorizationString(ServiceId aci, + ServiceId pni, + long redemptionTimeSeconds, GroupSecretParams groupSecretParams, - AuthCredentialResponse authCredentialResponse) + AuthCredentialWithPniResponse authCredentialWithPniResponse) throws VerificationFailedException { ClientZkAuthOperations authOperations = groupsOperations.getAuthOperations(); - AuthCredential authCredential = authOperations.receiveAuthCredential(self.uuid(), today, authCredentialResponse); - AuthCredentialPresentation authCredentialPresentation = authOperations.createAuthCredentialPresentation(new SecureRandom(), groupSecretParams, authCredential); + AuthCredentialWithPni authCredentialWithPni = authOperations.receiveAuthCredentialWithPni(aci.uuid(), pni.uuid(), redemptionTimeSeconds, authCredentialWithPniResponse); + AuthCredentialPresentation authCredentialPresentation = authOperations.createAuthCredentialPresentation(new SecureRandom(), groupSecretParams, authCredentialWithPni); return new GroupsV2AuthorizationString(groupSecretParams, authCredentialPresentation); } @@ -171,20 +174,20 @@ public class GroupsV2Api { return socket.getGroupExternalCredential(authorization); } - private static HashMap parseCredentialResponse(CredentialResponse credentialResponse) + private static HashMap parseCredentialResponse(CredentialResponse credentialResponse) throws IOException { - HashMap credentials = new HashMap<>(); + HashMap credentials = new HashMap<>(); for (TemporalCredential credential : credentialResponse.getCredentials()) { - AuthCredentialResponse authCredentialResponse; + AuthCredentialWithPniResponse authCredentialWithPniResponse; try { - authCredentialResponse = new AuthCredentialResponse(credential.getCredential()); + authCredentialWithPniResponse = new AuthCredentialWithPniResponse(credential.getCredential()); } catch (InvalidInputException e) { throw new IOException(e); } - credentials.put(credential.getRedemptionTime(), authCredentialResponse); + credentials.put(credential.getRedemptionTime(), authCredentialWithPniResponse); } return credentials; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java index a880c1964..e1aa2cab4 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java @@ -14,8 +14,8 @@ import org.signal.libsignal.zkgroup.groups.GroupSecretParams; import org.signal.libsignal.zkgroup.groups.ProfileKeyCiphertext; import org.signal.libsignal.zkgroup.groups.UuidCiphertext; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialPresentation; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.BannedMember; @@ -39,6 +39,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember; import org.signal.storageservice.protos.groups.local.DecryptedString; import org.signal.storageservice.protos.groups.local.DecryptedTimer; import org.signal.storageservice.protos.groups.local.EnabledState; +import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.util.UuidUtil; import java.security.SecureRandom; @@ -109,13 +110,13 @@ public final class GroupsV2Operations { .setAttributes(AccessControl.AccessRequired.MEMBER) .setMembers(AccessControl.AccessRequired.MEMBER)); - group.addMembers(groupOperations.member(self.getProfileKeyCredential().get(), Member.Role.ADMINISTRATOR)); + group.addMembers(groupOperations.member(self.requireExpiringProfileKeyCredential(), Member.Role.ADMINISTRATOR)); for (GroupCandidate credential : members) { - ProfileKeyCredential profileKeyCredential = credential.getProfileKeyCredential().orElse(null); + ExpiringProfileKeyCredential expiringProfileKeyCredential = credential.getExpiringProfileKeyCredential().orElse(null); - if (profileKeyCredential != null) { - group.addMembers(groupOperations.member(profileKeyCredential, memberRole)); + if (expiringProfileKeyCredential != null) { + group.addMembers(groupOperations.member(expiringProfileKeyCredential, memberRole)); } else { group.addPendingMembers(groupOperations.invitee(credential.getUuid(), memberRole)); } @@ -172,13 +173,13 @@ public final class GroupsV2Operations { : createUnbanUuidsChange(membersToUnban); for (GroupCandidate credential : membersToAdd) { - Member.Role newMemberRole = Member.Role.DEFAULT; - ProfileKeyCredential profileKeyCredential = credential.getProfileKeyCredential().orElse(null); + Member.Role newMemberRole = Member.Role.DEFAULT; + ExpiringProfileKeyCredential expiringProfileKeyCredential = credential.getExpiringProfileKeyCredential().orElse(null); - if (profileKeyCredential != null) { + if (expiringProfileKeyCredential != null) { actions.addAddMembers(GroupChange.Actions.AddMemberAction .newBuilder() - .setAdded(groupOperations.member(profileKeyCredential, newMemberRole))); + .setAdded(groupOperations.member(expiringProfileKeyCredential, newMemberRole))); } else { actions.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction .newBuilder() @@ -190,24 +191,24 @@ public final class GroupsV2Operations { return actions; } - public GroupChange.Actions.Builder createGroupJoinRequest(ProfileKeyCredential profileKeyCredential) { + public GroupChange.Actions.Builder createGroupJoinRequest(ExpiringProfileKeyCredential expiringProfileKeyCredential) { GroupOperations groupOperations = forGroup(groupSecretParams); GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder(); actions.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction .newBuilder() - .setAdded(groupOperations.requestingMember(profileKeyCredential))); + .setAdded(groupOperations.requestingMember(expiringProfileKeyCredential))); return actions; } - public GroupChange.Actions.Builder createGroupJoinDirect(ProfileKeyCredential profileKeyCredential) { + public GroupChange.Actions.Builder createGroupJoinDirect(ExpiringProfileKeyCredential expiringProfileKeyCredential) { GroupOperations groupOperations = forGroup(groupSecretParams); GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder(); actions.addAddMembers(GroupChange.Actions.AddMemberAction .newBuilder() - .setAdded(groupOperations.member(profileKeyCredential, Member.Role.DEFAULT))); + .setAdded(groupOperations.member(expiringProfileKeyCredential, Member.Role.DEFAULT))); return actions; } @@ -272,8 +273,8 @@ public final class GroupsV2Operations { .setTimer(encryptTimer(timerDurationSeconds))); } - public GroupChange.Actions.Builder createUpdateProfileKeyCredentialChange(ProfileKeyCredential profileKeyCredential) { - ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(random, groupSecretParams, profileKeyCredential); + public GroupChange.Actions.Builder createUpdateProfileKeyCredentialChange(ExpiringProfileKeyCredential expiringProfileKeyCredential) { + ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(random, groupSecretParams, expiringProfileKeyCredential); return GroupChange.Actions .newBuilder() @@ -282,14 +283,20 @@ public final class GroupsV2Operations { .setPresentation(ByteString.copyFrom(presentation.serialize()))); } - public GroupChange.Actions.Builder createAcceptInviteChange(ProfileKeyCredential profileKeyCredential) { - ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(random, groupSecretParams, profileKeyCredential); + public GroupChange.Actions.Builder createAcceptInviteChange(ExpiringProfileKeyCredential credential) { + ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(random, groupSecretParams, credential); - return GroupChange.Actions - .newBuilder() - .addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction - .newBuilder() - .setPresentation(ByteString.copyFrom(presentation.serialize()))); + return GroupChange.Actions.newBuilder() + .addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder() + .setPresentation(ByteString.copyFrom(presentation.serialize()))); + } + + public GroupChange.Actions.Builder createAcceptPniInviteChange(ExpiringProfileKeyCredential credential) { + ByteString presentation = ByteString.copyFrom(clientZkProfileOperations.createProfileKeyCredentialPresentation(random, groupSecretParams, credential).serialize()); + + return GroupChange.Actions.newBuilder() + .addPromotePendingPniAciMembers(GroupChange.Actions.PromotePendingPniAciMemberProfileKeyAction.newBuilder() + .setPresentation(presentation)); } public GroupChange.Actions.Builder createRemoveInvitationChange(final Set uuidCipherTextsFromInvitesToRemove) { @@ -387,7 +394,30 @@ public final class GroupsV2Operations { return builder; } - private Member.Builder member(ProfileKeyCredential credential, Member.Role role) { + public GroupChange.Actions.Builder replaceAddMembers(GroupChange.Actions.Builder change, List candidates) throws InvalidInputException { + if (change.getAddMembersCount() != candidates.size()) { + throw new InvalidInputException("Replacement candidates not same size as original add"); + } + + for (int i = 0; i < change.getAddMembersCount(); i++) { + GroupChange.Actions.AddMemberAction original = change.getAddMembers(i); + GroupCandidate candidate = candidates.get(i); + + ExpiringProfileKeyCredential expiringProfileKeyCredential = candidate.getExpiringProfileKeyCredential().orElse(null); + + if (expiringProfileKeyCredential == null) { + throw new InvalidInputException("Replacement candidate missing credential"); + } + + change.setAddMembers(i, + GroupChange.Actions.AddMemberAction.newBuilder() + .setAdded(member(expiringProfileKeyCredential, original.getAdded().getRole()))); + } + + return change; + } + + private Member.Builder member(ExpiringProfileKeyCredential credential, Member.Role role) { ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(new SecureRandom(), groupSecretParams, credential); return Member.newBuilder() @@ -395,7 +425,7 @@ public final class GroupsV2Operations { .setPresentation(ByteString.copyFrom(presentation.serialize())); } - private RequestingMember.Builder requestingMember(ProfileKeyCredential credential) { + private RequestingMember.Builder requestingMember(ExpiringProfileKeyCredential credential) { ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(new SecureRandom(), groupSecretParams, credential); return RequestingMember.newBuilder() @@ -559,15 +589,23 @@ public final class GroupsV2Operations { // Field 6 for (GroupChange.Actions.ModifyMemberProfileKeyAction modifyMemberProfileKeyAction : actions.getModifyMemberProfileKeysList()) { try { - ProfileKeyCredentialPresentation presentation = new ProfileKeyCredentialPresentation(modifyMemberProfileKeyAction.getPresentation().toByteArray()); - presentation.getProfileKeyCiphertext(); + UUID uuid; + ProfileKey profileKey; + + if (modifyMemberProfileKeyAction.getUserId().isEmpty() || modifyMemberProfileKeyAction.getProfileKey().isEmpty()) { + ProfileKeyCredentialPresentation presentation = new ProfileKeyCredentialPresentation(modifyMemberProfileKeyAction.getPresentation().toByteArray()); + uuid = decryptUuid(ByteString.copyFrom(presentation.getUuidCiphertext().serialize())); + profileKey = decryptProfileKey(ByteString.copyFrom(presentation.getProfileKeyCiphertext().serialize()), uuid); + } else { + uuid = decryptUuid(modifyMemberProfileKeyAction.getUserId()); + profileKey = decryptProfileKey(modifyMemberProfileKeyAction.getProfileKey(), uuid); + } - UUID uuid = decryptUuid(ByteString.copyFrom(presentation.getUuidCiphertext().serialize())); builder.addModifiedProfileKeys(DecryptedMember.newBuilder() .setRole(Member.Role.UNKNOWN) .setJoinedAtRevision(-1) .setUuid(UuidUtil.toByteString(uuid)) - .setProfileKey(ByteString.copyFrom(decryptProfileKey(ByteString.copyFrom(presentation.getProfileKeyCiphertext().serialize()), uuid).serialize()))); + .setProfileKey(ByteString.copyFrom(profileKey.serialize()))); } catch (InvalidInputException e) { throw new InvalidGroupStateException(e); } @@ -600,19 +638,27 @@ public final class GroupsV2Operations { // Field 9 for (GroupChange.Actions.PromotePendingMemberAction promotePendingMemberAction : actions.getPromotePendingMembersList()) { - ProfileKeyCredentialPresentation profileKeyCredentialPresentation; try { - profileKeyCredentialPresentation = new ProfileKeyCredentialPresentation(promotePendingMemberAction.getPresentation().toByteArray()); + UUID uuid; + ProfileKey profileKey; + + if (promotePendingMemberAction.getUserId().isEmpty() || promotePendingMemberAction.getProfileKey().isEmpty()) { + ProfileKeyCredentialPresentation presentation = new ProfileKeyCredentialPresentation(promotePendingMemberAction.getPresentation().toByteArray()); + uuid = decryptUuid(ByteString.copyFrom(presentation.getUuidCiphertext().serialize())); + profileKey = decryptProfileKey(ByteString.copyFrom(presentation.getProfileKeyCiphertext().serialize()), uuid); + } else { + uuid = decryptUuid(promotePendingMemberAction.getUserId()); + profileKey = decryptProfileKey(promotePendingMemberAction.getProfileKey(), uuid); + } + + builder.addPromotePendingMembers(DecryptedMember.newBuilder() + .setJoinedAtRevision(-1) + .setRole(Member.Role.DEFAULT) + .setUuid(UuidUtil.toByteString(uuid)) + .setProfileKey(ByteString.copyFrom(profileKey.serialize()))); } catch (InvalidInputException e) { throw new InvalidGroupStateException(e); } - UUID uuid = clientZkGroupCipher.decryptUuid(profileKeyCredentialPresentation.getUuidCiphertext()); - ProfileKey profileKey = clientZkGroupCipher.decryptProfileKey(profileKeyCredentialPresentation.getProfileKeyCiphertext(), uuid); - builder.addPromotePendingMembers(DecryptedMember.newBuilder() - .setJoinedAtRevision(-1) - .setRole(Member.Role.DEFAULT) - .setUuid(UuidUtil.toByteString(uuid)) - .setProfileKey(ByteString.copyFrom(profileKey.serialize()))); } // Field 10 @@ -686,6 +732,21 @@ public final class GroupsV2Operations { builder.addDeleteBannedMembers(DecryptedBannedMember.newBuilder().setUuid(decryptUuidToByteString(action.getDeletedUserId())).build()); } + // Field 24 + for (GroupChange.Actions.PromotePendingPniAciMemberProfileKeyAction promotePendingPniAciMemberAction : actions.getPromotePendingPniAciMembersList()) { + UUID uuid = decryptUuid(promotePendingPniAciMemberAction.getUserId()); + UUID pni = decryptUuid(promotePendingPniAciMemberAction.getPni()); + ProfileKey profileKey = decryptProfileKey(promotePendingPniAciMemberAction.getProfileKey(), uuid); + + builder.setEditor(UuidUtil.toByteString(uuid)) + .addPromotePendingPniAciMembers(DecryptedMember.newBuilder() + .setUuid(UuidUtil.toByteString(uuid)) + .setRole(Member.Role.DEFAULT) + .setProfileKey(ByteString.copyFrom(profileKey.serialize())) + .setJoinedAtRevision(actions.getRevision()) + .setPni(UuidUtil.toByteString(pni))); + } + return builder.build(); } @@ -920,6 +981,18 @@ public final class GroupsV2Operations { .setUserId(encryptUuid(uuid)) .setRole(role)); } + + public List decryptAddMembers(List addMembers) throws InvalidInputException, VerificationFailedException { + List ids = new ArrayList<>(addMembers.size()); + for (int i = 0; i < addMembers.size(); i++) { + GroupChange.Actions.AddMemberAction addMember = addMembers.get(i); + ProfileKeyCredentialPresentation profileKeyCredentialPresentation = new ProfileKeyCredentialPresentation(addMember.getAdded().getPresentation().toByteArray()); + + ids.add(ServiceId.from(clientZkGroupCipher.decryptUuid(profileKeyCredentialPresentation.getUuidCiphertext()))); + } + return ids; + } + } public static class NewGroup { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/TemporalCredential.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/TemporalCredential.java index cf84fb486..477961924 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/TemporalCredential.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/TemporalCredential.java @@ -8,13 +8,13 @@ public class TemporalCredential { private byte[] credential; @JsonProperty - private int redemptionTime; + private long redemptionTime; public byte[] getCredential() { return credential; } - public int getRedemptionTime() { + public long getRedemptionTime() { return redemptionTime; } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/ProfileAndCredential.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/ProfileAndCredential.java index 87f9b71fa..abe00ca61 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/ProfileAndCredential.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/ProfileAndCredential.java @@ -1,23 +1,23 @@ package org.whispersystems.signalservice.api.profiles; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import java.util.Optional; public final class ProfileAndCredential { - private final SignalServiceProfile profile; - private final SignalServiceProfile.RequestType requestType; - private final Optional profileKeyCredential; + private final SignalServiceProfile profile; + private final SignalServiceProfile.RequestType requestType; + private final Optional expiringProfileKeyCredential; public ProfileAndCredential(SignalServiceProfile profile, SignalServiceProfile.RequestType requestType, - Optional profileKeyCredential) + Optional expiringProfileKeyCredential) { - this.profile = profile; - this.requestType = requestType; - this.profileKeyCredential = profileKeyCredential; + this.profile = profile; + this.requestType = requestType; + this.expiringProfileKeyCredential = expiringProfileKeyCredential; } public SignalServiceProfile getProfile() { @@ -28,7 +28,7 @@ public final class ProfileAndCredential { return requestType; } - public Optional getProfileKeyCredential() { - return profileKeyCredential; + public Optional getExpiringProfileKeyCredential() { + return expiringProfileKeyCredential; } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java index ce71132be..c4a4b9d8a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java @@ -9,7 +9,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.signal.libsignal.protocol.logging.Log; import org.signal.libsignal.zkgroup.InvalidInputException; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialResponse; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.internal.util.JsonUtil; @@ -245,11 +245,11 @@ public class SignalServiceProfile { } } - public ProfileKeyCredentialResponse getProfileKeyCredentialResponse() { + public ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse() { if (credential == null) return null; try { - return new ProfileKeyCredentialResponse(credential); + return new ExpiringProfileKeyCredentialResponse(credential); } catch (InvalidInputException e) { Log.w(TAG, e); return null; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/ServiceIds.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/ServiceIds.java new file mode 100644 index 000000000..22d267657 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/ServiceIds.java @@ -0,0 +1,54 @@ +package org.whispersystems.signalservice.api.push; + +import com.google.protobuf.ByteString; + +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.util.Objects; +import java.util.UUID; + +/** + * Helper for dealing with [ServiceId] matching when you only care that either of your + * service ids match but don't care which one. + */ +public final class ServiceIds { + + private final ACI aci; + private final PNI pni; + + private ByteString aciByteString; + private ByteString pniByteString; + + public ServiceIds(ACI aci, PNI pni) { + this.aci = aci; + this.pni = pni; + } + + public ACI getAci() { + return aci; + } + + public PNI getPni() { + return pni; + } + + public PNI requirePni() { + return Objects.requireNonNull(pni); + } + + public boolean matches(UUID uuid) { + return uuid.equals(aci.uuid()) || (pni != null && uuid.equals(pni.uuid())); + } + + public boolean matches(ByteString uuid) { + if (aciByteString == null) { + aciByteString = aci.toByteString(); + } + + if (pniByteString == null && pni != null) { + pniByteString = pni.toByteString(); + } + + return uuid.equals(aciByteString) || uuid.equals(pniByteString); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/ProfileService.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/ProfileService.java index dd8c5ad99..dace6a7e5 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/ProfileService.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/ProfileService.java @@ -4,8 +4,8 @@ import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.util.Pair; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequestContext; import org.signal.libsignal.zkgroup.profiles.ProfileKeyVersion; @@ -90,7 +90,7 @@ public final class ProfileService { ProfileKeyCredentialRequest request = requestContext.getRequest(); String credentialRequest = Hex.toStringCondensed(request.serialize()); - builder.setPath(String.format("/v1/profile/%s/%s/%s", serviceId, version, credentialRequest)); + builder.setPath(String.format("/v1/profile/%s/%s/%s?credentialType=expiringProfileKey", serviceId, version, credentialRequest)); } else { builder.setPath(String.format("/v1/profile/%s/%s", serviceId, version)); } @@ -170,13 +170,13 @@ public final class ProfileService { throws MalformedResponseException { try { - SignalServiceProfile signalServiceProfile = JsonUtil.fromJsonResponse(body, SignalServiceProfile.class); - ProfileKeyCredential profileKeyCredential = null; - if (requestContext != null && signalServiceProfile.getProfileKeyCredentialResponse() != null) { - profileKeyCredential = clientZkProfileOperations.receiveProfileKeyCredential(requestContext, signalServiceProfile.getProfileKeyCredentialResponse()); + SignalServiceProfile signalServiceProfile = JsonUtil.fromJsonResponse(body, SignalServiceProfile.class); + ExpiringProfileKeyCredential expiringProfileKeyCredential = null; + if (requestContext != null && signalServiceProfile.getExpiringProfileKeyCredentialResponse() != null) { + expiringProfileKeyCredential = clientZkProfileOperations.receiveExpiringProfileKeyCredential(requestContext, signalServiceProfile.getExpiringProfileKeyCredentialResponse()); } - return ServiceResponse.forResult(new ProfileAndCredential(signalServiceProfile, requestType, Optional.ofNullable(profileKeyCredential)), status, body); + return ServiceResponse.forResult(new ProfileAndCredential(signalServiceProfile, requestType, Optional.ofNullable(expiringProfileKeyCredential)), status, body); } catch (VerificationFailedException e) { return ServiceResponse.forApplicationError(e, status, body); } @@ -204,5 +204,10 @@ public final class ProfileService { public boolean genericIoError() { return super.genericIoError(); } + + @Override + public Throwable getError() { + return super.getError(); + } } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/ExpiringProfileCredentialUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/ExpiringProfileCredentialUtil.java new file mode 100644 index 000000000..f25c8d1a9 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/ExpiringProfileCredentialUtil.java @@ -0,0 +1,21 @@ +package org.whispersystems.signalservice.api.util; + +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; + +import java.time.temporal.ChronoField; +import java.util.concurrent.TimeUnit; + +public final class ExpiringProfileCredentialUtil { + + private ExpiringProfileCredentialUtil() {} + + public static boolean isValid(ExpiringProfileKeyCredential credential) { + if (credential == null) { + return false; + } + + long now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()); + long expires = credential.getExpirationTime().getLong(ChronoField.INSTANT_SECONDS); + return now < expires; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index c8e9b9271..06de2fd0e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -22,6 +22,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.util.Pair; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest; @@ -236,7 +237,7 @@ public class PushServiceSocket { private static final String STICKER_MANIFEST_PATH = "stickers/%s/manifest.proto"; private static final String STICKER_PATH = "stickers/%s/full/%d"; - private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d?identity=%s"; + private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/auth/group?redemptionStartSeconds=%d&redemptionEndSeconds=%d"; private static final String GROUPSV2_GROUP = "/v1/groups/"; private static final String GROUPSV2_GROUP_PASSWORD = "/v1/groups/?inviteLinkPassword=%s"; private static final String GROUPSV2_GROUP_CHANGES = "/v1/groups/logs/%s?maxSupportedChangeEpoch=%d&includeFirstState=%s&includeLastState=false"; @@ -774,7 +775,7 @@ public class PushServiceSocket { String version = profileKeyIdentifier.serialize(); String credentialRequest = Hex.toStringCondensed(request.serialize()); - String subPath = String.format("%s/%s/%s", target, version, credentialRequest); + String subPath = String.format("%s/%s/%s?credentialType=expiringProfileKey", target, version, credentialRequest); ListenableFuture response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), unidentifiedAccess); @@ -789,10 +790,10 @@ public class PushServiceSocket { SignalServiceProfile signalServiceProfile = JsonUtil.fromJson(body, SignalServiceProfile.class); try { - ProfileKeyCredential profileKeyCredential = signalServiceProfile.getProfileKeyCredentialResponse() != null - ? clientZkProfileOperations.receiveProfileKeyCredential(requestContext, signalServiceProfile.getProfileKeyCredentialResponse()) - : null; - return new ProfileAndCredential(signalServiceProfile, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, Optional.ofNullable(profileKeyCredential)); + ExpiringProfileKeyCredential expiringProfileKeyCredential = signalServiceProfile.getExpiringProfileKeyCredentialResponse() != null + ? clientZkProfileOperations.receiveExpiringProfileKeyCredential(requestContext, signalServiceProfile.getExpiringProfileKeyCredentialResponse()) + : null; + return new ProfileAndCredential(signalServiceProfile, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, Optional.ofNullable(expiringProfileKeyCredential)); } catch (VerificationFailedException e) { Log.w(TAG, "Failed to verify credential.", e); return new ProfileAndCredential(signalServiceProfile, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, Optional.empty()); @@ -2339,15 +2340,15 @@ public class PushServiceSocket { public enum ClientSet { ContactDiscovery, KeyBackup } - public CredentialResponse retrieveGroupsV2Credentials(int today, boolean isAci) + public CredentialResponse retrieveGroupsV2Credentials(long todaySeconds) throws IOException { - int todayPlus7 = today + 7; - String response = makeServiceRequest(String.format(Locale.US, GROUPSV2_CREDENTIAL, today, todayPlus7, isAci ? "aci" : "pni"), - "GET", - null, - NO_HEADERS, - Optional.empty()); + long todayPlus7 = todaySeconds + TimeUnit.DAYS.toSeconds(7); + String response = makeServiceRequest(String.format(Locale.US, GROUPSV2_CREDENTIAL, todaySeconds, todayPlus7), + "GET", + null, + NO_HEADERS, + Optional.empty()); return JsonUtil.fromJson(response, CredentialResponse.class); } diff --git a/libsignal/service/src/main/proto/DecryptedGroups.proto b/libsignal/service/src/main/proto/DecryptedGroups.proto index 0c10c5c79..45e6cde52 100644 --- a/libsignal/service/src/main/proto/DecryptedGroups.proto +++ b/libsignal/service/src/main/proto/DecryptedGroups.proto @@ -17,6 +17,7 @@ message DecryptedMember { Member.Role role = 2; bytes profileKey = 3; uint32 joinedAtRevision = 5; + bytes pni = 6; } message DecryptedPendingMember { @@ -73,29 +74,30 @@ message DecryptedGroup { // Decrypted version of message GroupChange.Actions // Keep field numbers in step message DecryptedGroupChange { - bytes editor = 1; - uint32 revision = 2; - repeated DecryptedMember newMembers = 3; - repeated bytes deleteMembers = 4; - repeated DecryptedModifyMemberRole modifyMemberRoles = 5; - repeated DecryptedMember modifiedProfileKeys = 6; - repeated DecryptedPendingMember newPendingMembers = 7; - repeated DecryptedPendingMemberRemoval deletePendingMembers = 8; - repeated DecryptedMember promotePendingMembers = 9; - DecryptedString newTitle = 10; - DecryptedString newAvatar = 11; - DecryptedTimer newTimer = 12; - AccessControl.AccessRequired newAttributeAccess = 13; - AccessControl.AccessRequired newMemberAccess = 14; - AccessControl.AccessRequired newInviteLinkAccess = 15; - repeated DecryptedRequestingMember newRequestingMembers = 16; - repeated bytes deleteRequestingMembers = 17; - repeated DecryptedApproveMember promoteRequestingMembers = 18; - bytes newInviteLinkPassword = 19; - DecryptedString newDescription = 20; - EnabledState newIsAnnouncementGroup = 21; - repeated DecryptedBannedMember newBannedMembers = 22; - repeated DecryptedBannedMember deleteBannedMembers = 23; + bytes editor = 1; + uint32 revision = 2; + repeated DecryptedMember newMembers = 3; + repeated bytes deleteMembers = 4; + repeated DecryptedModifyMemberRole modifyMemberRoles = 5; + repeated DecryptedMember modifiedProfileKeys = 6; + repeated DecryptedPendingMember newPendingMembers = 7; + repeated DecryptedPendingMemberRemoval deletePendingMembers = 8; + repeated DecryptedMember promotePendingMembers = 9; + DecryptedString newTitle = 10; + DecryptedString newAvatar = 11; + DecryptedTimer newTimer = 12; + AccessControl.AccessRequired newAttributeAccess = 13; + AccessControl.AccessRequired newMemberAccess = 14; + AccessControl.AccessRequired newInviteLinkAccess = 15; + repeated DecryptedRequestingMember newRequestingMembers = 16; + repeated bytes deleteRequestingMembers = 17; + repeated DecryptedApproveMember promoteRequestingMembers = 18; + bytes newInviteLinkPassword = 19; + DecryptedString newDescription = 20; + EnabledState newIsAnnouncementGroup = 21; + repeated DecryptedBannedMember newBannedMembers = 22; + repeated DecryptedBannedMember deleteBannedMembers = 23; + repeated DecryptedMember promotePendingPniAciMembers = 24; } message DecryptedString { diff --git a/libsignal/service/src/main/proto/Groups.proto b/libsignal/service/src/main/proto/Groups.proto index 7233a892f..e462536f2 100644 --- a/libsignal/service/src/main/proto/Groups.proto +++ b/libsignal/service/src/main/proto/Groups.proto @@ -99,7 +99,9 @@ message GroupChange { } message ModifyMemberProfileKeyAction { - bytes presentation = 1; + bytes presentation = 1; // Only set when sending to server + bytes user_id = 2; // Only set when receiving from server + bytes profile_key = 3; // Only set when receiving from server } message AddPendingMemberAction { @@ -111,7 +113,16 @@ message GroupChange { } message PromotePendingMemberAction { - bytes presentation = 1; + bytes presentation = 1; // Only set when sending to server + bytes user_id = 2; // Only set when receiving from server + bytes profile_key = 3; // Only set when receiving from server + } + + message PromotePendingPniAciMemberProfileKeyAction { + bytes presentation = 1; // Only set when sending to server + bytes userId = 2; // Only set when receiving from server + bytes pni = 3; // Only set when receiving from server + bytes profileKey = 4; // Only set when receiving from server } message AddRequestingMemberAction { @@ -194,6 +205,7 @@ message GroupChange { ModifyAnnouncementsOnlyAction modifyAnnouncementsOnly = 21; repeated AddBannedMemberAction addBannedMembers = 22; repeated DeleteBannedMemberAction deleteBannedMembers = 23; + repeated PromotePendingPniAciMemberProfileKeyAction promotePendingPniAciMembers = 24; } bytes actions = 1; diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_apply_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_apply_Test.java index 7979835a8..18c8c7986 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_apply_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_apply_Test.java @@ -31,6 +31,7 @@ import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.banne import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.newProfileKey; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember; +import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingPniAciMember; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.requestingMember; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.withProfileKey; @@ -48,7 +49,7 @@ public final class DecryptedGroupUtil_apply_Test { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class); assertEquals("DecryptedGroupUtil and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(), - 23, maxFieldFound); + 24, maxFieldFound); } @Test @@ -958,4 +959,31 @@ public final class DecryptedGroupUtil_apply_Test { .build(), newGroup); } + + @Test + public void promote_pending_member_pni_aci() throws NotAbleToApplyGroupV2ChangeException { + ProfileKey profileKey2 = randomProfileKey(); + DecryptedMember member1 = member(UUID.randomUUID()); + UUID pending2Aci = UUID.randomUUID(); + UUID pending2Pni = UUID.randomUUID(); + DecryptedPendingMember pending2 = pendingMember(pending2Pni); + DecryptedMember member2 = pendingPniAciMember(pending2Aci, pending2Pni, profileKey2); + + DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder() + .setRevision(10) + .addMembers(member1) + .addPendingMembers(pending2) + .build(), + DecryptedGroupChange.newBuilder() + .setRevision(11) + .addPromotePendingPniAciMembers(member2) + .build()); + + assertEquals(DecryptedGroup.newBuilder() + .setRevision(11) + .addMembers(member1) + .addMembers(member2) + .build(), + newGroup); + } } diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_empty_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_empty_Test.java index e64014cee..43c38cf4e 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_empty_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_empty_Test.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember; +import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingPniAciMember; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMemberRemoval; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.promoteAdmin; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey; @@ -37,7 +38,7 @@ public final class DecryptedGroupUtil_empty_Test { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class); assertEquals("DecryptedGroupUtil and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(), - 23, maxFieldFound); + 24, maxFieldFound); } @Test @@ -254,4 +255,14 @@ public final class DecryptedGroupUtil_empty_Test { assertFalse(DecryptedGroupUtil.changeIsEmpty(change)); assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change)); } + + @Test + public void not_empty_with_promote_pending_pni_aci_members_field_24() { + DecryptedGroupChange change = DecryptedGroupChange.newBuilder() + .addPromotePendingPniAciMembers(pendingPniAciMember(UUID.randomUUID(), UUID.randomUUID(), randomProfileKey())) + .build(); + + assertFalse(DecryptedGroupUtil.changeIsEmpty(change)); + assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change)); + } } diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_changeIsEmpty_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_changeIsEmpty_Test.java index 23914c91d..994a21325 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_changeIsEmpty_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_changeIsEmpty_Test.java @@ -20,7 +20,7 @@ public final class GroupChangeUtil_changeIsEmpty_Test { int maxFieldFound = getMaxDeclaredFieldNumber(GroupChange.Actions.class); assertEquals("GroupChangeUtil and its tests need updating to account for new fields on " + GroupChange.Actions.class.getName(), - 23, maxFieldFound); + 24, maxFieldFound); } @Test @@ -216,4 +216,13 @@ public final class GroupChangeUtil_changeIsEmpty_Test { assertFalse(GroupChangeUtil.changeIsEmpty(actions)); } + + @Test + public void not_empty_with_promote_pending_pni_aci_members_field_24() { + GroupChange.Actions actions = GroupChange.Actions.newBuilder() + .addPromotePendingPniAciMembers(GroupChange.Actions.PromotePendingPniAciMemberProfileKeyAction.getDefaultInstance()) + .build(); + + assertFalse(GroupChangeUtil.changeIsEmpty(actions)); + } } diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_Test.java index cf63c2783..5e304a829 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_Test.java @@ -11,6 +11,7 @@ import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.PendingMember; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; +import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedString; import org.signal.storageservice.protos.groups.local.DecryptedTimer; import org.signal.storageservice.protos.groups.local.EnabledState; @@ -31,6 +32,7 @@ import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.encry import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMemberRemoval; +import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingPniAciMember; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.presentation; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.promoteAdmin; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey; @@ -49,7 +51,7 @@ public final class GroupChangeUtil_resolveConflict_Test { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class); assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(), - 23, maxFieldFound); + 24, maxFieldFound); } /** @@ -62,7 +64,7 @@ public final class GroupChangeUtil_resolveConflict_Test { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class); assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + GroupChange.class.getName(), - 23, maxFieldFound); + 24, maxFieldFound); } /** @@ -802,4 +804,31 @@ public final class GroupChangeUtil_resolveConflict_Test { .build(); assertEquals(expected, resolvedActions); } + + @Test + public void field_24__promote_pending_members() { + DecryptedMember member1 = pendingPniAciMember(UUID.randomUUID(), UUID.randomUUID(), randomProfileKey()); + DecryptedMember member2 = pendingPniAciMember(UUID.randomUUID(), UUID.randomUUID(), randomProfileKey()); + + DecryptedGroup groupState = DecryptedGroup.newBuilder() + .addMembers(member(UuidUtil.fromByteString(member1.getUuid()))) + .build(); + + DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder() + .addPromotePendingPniAciMembers(pendingPniAciMember(member1.getUuid(), member1.getPni(), member1.getProfileKey())) + .addPromotePendingPniAciMembers(pendingPniAciMember(member2.getUuid(), member2.getPni(), member2.getProfileKey())) + .build(); + + GroupChange.Actions change = GroupChange.Actions.newBuilder() + .addPromotePendingPniAciMembers(GroupChange.Actions.PromotePendingPniAciMemberProfileKeyAction.newBuilder().setPresentation(presentation(member1.getPni(), member1.getProfileKey()))) + .addPromotePendingPniAciMembers(GroupChange.Actions.PromotePendingPniAciMemberProfileKeyAction.newBuilder().setPresentation(presentation(member2.getPni(), member2.getProfileKey()))) + .build(); + + GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build(); + + GroupChange.Actions expected = GroupChange.Actions.newBuilder() + .addPromotePendingPniAciMembers(GroupChange.Actions.PromotePendingPniAciMemberProfileKeyAction.newBuilder().setPresentation(presentation(member2.getPni(), member2.getProfileKey()))) + .build(); + assertEquals(expected, resolvedActions); + } } \ No newline at end of file diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_decryptedOnly_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_decryptedOnly_Test.java index 89c988aee..748acb19f 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_decryptedOnly_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_decryptedOnly_Test.java @@ -5,8 +5,10 @@ import com.google.protobuf.ByteString; import org.junit.Test; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.storageservice.protos.groups.AccessControl; +import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; +import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedString; import org.signal.storageservice.protos.groups.local.DecryptedTimer; import org.signal.storageservice.protos.groups.local.EnabledState; @@ -24,6 +26,8 @@ import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.demot import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMemberRemoval; +import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingPniAciMember; +import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.presentation; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.promoteAdmin; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey; import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.requestingMember; @@ -41,7 +45,7 @@ public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class); assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(), - 23, maxFieldFound); + 24, maxFieldFound); } /** @@ -650,4 +654,27 @@ public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test { assertEquals(expected, resolvedChanges); } + + @Test + public void field_24__promote_pending_members() { + DecryptedMember member1 = pendingPniAciMember(UUID.randomUUID(), UUID.randomUUID(), randomProfileKey()); + DecryptedMember member2 = pendingPniAciMember(UUID.randomUUID(), UUID.randomUUID(), randomProfileKey()); + + DecryptedGroup groupState = DecryptedGroup.newBuilder() + .addMembers(member(UuidUtil.fromByteString(member1.getUuid()))) + .build(); + + DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder() + .addPromotePendingPniAciMembers(pendingPniAciMember(member1.getUuid(), member1.getPni(), member1.getProfileKey())) + .addPromotePendingPniAciMembers(pendingPniAciMember(member2.getUuid(), member2.getPni(), member2.getProfileKey())) + .build(); + + DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build(); + + DecryptedGroupChange expected = DecryptedGroupChange.newBuilder() + .addPromotePendingPniAciMembers(pendingPniAciMember(member2.getUuid(), member2.getPni(), member2.getProfileKey())) + .build(); + + assertEquals(expected, resolvedChanges); + } } \ No newline at end of file diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java index c47544ffb..8398586dd 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java @@ -7,17 +7,18 @@ import org.junit.Before; import org.junit.Test; import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.VerificationFailedException; +import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher; import org.signal.libsignal.zkgroup.groups.GroupMasterKey; import org.signal.libsignal.zkgroup.groups.GroupSecretParams; import org.signal.libsignal.zkgroup.groups.UuidCiphertext; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialPresentation; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequestContext; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialResponse; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.Member; @@ -36,6 +37,8 @@ import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.signalservice.testutil.LibSignalLibraryUtil; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.Optional; import java.util.UUID; @@ -66,7 +69,8 @@ public final class GroupsV2Operations_decrypt_change_Test { int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class); assertEquals("GroupV2Operations#decryptChange and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(), - 23, maxFieldFound); + 24, + maxFieldFound); } @Test @@ -112,7 +116,7 @@ public final class GroupsV2Operations_decrypt_change_Test { ProfileKey profileKey = newProfileKey(); GroupCandidate groupCandidate = groupCandidate(newMember, profileKey); - assertDecryption(groupOperations.createGroupJoinDirect(groupCandidate.getProfileKeyCredential().get()) + assertDecryption(groupOperations.createGroupJoinDirect(groupCandidate.getExpiringProfileKeyCredential().get()) .setRevision(10), DecryptedGroupChange.newBuilder() .setRevision(10) @@ -148,7 +152,7 @@ public final class GroupsV2Operations_decrypt_change_Test { actions.addAddMembers(GroupChange.Actions.AddMemberAction.newBuilder() .setAdded(Member.newBuilder().setRole(Member.Role.DEFAULT) - .setPresentation(ByteString.copyFrom(randomPresentation)))); + .setPresentation(ByteString.copyFrom(randomPresentation)))); groupOperations.decryptChange(GroupChange.newBuilder().setActions(actions.build().toByteString()).build(), false); } @@ -203,7 +207,7 @@ public final class GroupsV2Operations_decrypt_change_Test { ProfileKey profileKey = newProfileKey(); GroupCandidate groupCandidate = groupCandidate(self, profileKey); - assertDecryption(groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.getProfileKeyCredential().get()) + assertDecryption(groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.getExpiringProfileKeyCredential().get()) .setRevision(10), DecryptedGroupChange.newBuilder() .setRevision(10) @@ -230,7 +234,7 @@ public final class GroupsV2Operations_decrypt_change_Test { .setRole(Member.Role.DEFAULT) .setUuid(UuidUtil.toByteString(newMember)))); } - + @Test public void can_decrypt_pending_member_removals_field8() throws InvalidInputException { UUID oldMember = UUID.randomUUID(); @@ -248,9 +252,9 @@ public final class GroupsV2Operations_decrypt_change_Test { byte[] uuidCiphertext = Util.getSecretBytes(60); assertDecryption(GroupChange.Actions - .newBuilder() - .addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.newBuilder() - .setDeletedUserId(ByteString.copyFrom(uuidCiphertext))), + .newBuilder() + .addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.newBuilder() + .setDeletedUserId(ByteString.copyFrom(uuidCiphertext))), DecryptedGroupChange.newBuilder() .addDeletePendingMembers(DecryptedPendingMemberRemoval.newBuilder() .setUuid(UuidUtil.toByteString(UuidUtil.UNKNOWN_UUID)) @@ -263,7 +267,7 @@ public final class GroupsV2Operations_decrypt_change_Test { ProfileKey profileKey = newProfileKey(); GroupCandidate groupCandidate = groupCandidate(newMember, profileKey); - assertDecryption(groupOperations.createAcceptInviteChange(groupCandidate.getProfileKeyCredential().get()), + assertDecryption(groupOperations.createAcceptInviteChange(groupCandidate.getExpiringProfileKeyCredential().get()), DecryptedGroupChange.newBuilder() .addPromotePendingMembers(DecryptedMember.newBuilder() .setUuid(UuidUtil.toByteString(newMember)) @@ -328,7 +332,7 @@ public final class GroupsV2Operations_decrypt_change_Test { ProfileKey profileKey = newProfileKey(); GroupCandidate groupCandidate = groupCandidate(newRequestingMember, profileKey); - assertDecryption(groupOperations.createGroupJoinRequest(groupCandidate.getProfileKeyCredential().get()) + assertDecryption(groupOperations.createGroupJoinRequest(groupCandidate.getExpiringProfileKeyCredential().get()) .setRevision(10), DecryptedGroupChange.newBuilder() .setRevision(10) @@ -368,9 +372,9 @@ public final class GroupsV2Operations_decrypt_change_Test { assertDecryption(GroupChange.Actions.newBuilder() .setModifyInviteLinkPassword(GroupChange.Actions.ModifyInviteLinkPasswordAction.newBuilder() - .setInviteLinkPassword(ByteString.copyFrom(newPassword))), - DecryptedGroupChange.newBuilder() - .setNewInviteLinkPassword(ByteString.copyFrom(newPassword))); + .setInviteLinkPassword(ByteString.copyFrom(newPassword))), + DecryptedGroupChange.newBuilder() + .setNewInviteLinkPassword(ByteString.copyFrom(newPassword))); } @Test @@ -413,6 +417,32 @@ public final class GroupsV2Operations_decrypt_change_Test { .setUuid(UuidUtil.toByteString(ban)))); } + @Test + public void can_decrypt_promote_pending_pni_aci_member_field24() { + UUID memberUuid = UUID.randomUUID(); + UUID memberPni = UUID.randomUUID(); + ProfileKey profileKey = newProfileKey(); + + GroupChange.Actions.Builder builder = GroupChange.Actions.newBuilder() + .setSourceUuid(groupOperations.encryptUuid(memberPni)) + .setRevision(5) + .addPromotePendingPniAciMembers(GroupChange.Actions.PromotePendingPniAciMemberProfileKeyAction.newBuilder() + .setUserId(groupOperations.encryptUuid(memberUuid)) + .setPni(groupOperations.encryptUuid(memberPni)) + .setProfileKey(encryptProfileKey(memberUuid, profileKey))); + + assertDecryptionWithEditorSet(builder, + DecryptedGroupChange.newBuilder() + .setEditor(UuidUtil.toByteString(memberUuid)) + .setRevision(5) + .addPromotePendingPniAciMembers(DecryptedMember.newBuilder() + .setUuid(UuidUtil.toByteString(memberUuid)) + .setPni(UuidUtil.toByteString(memberPni)) + .setRole(Member.Role.DEFAULT) + .setProfileKey(ByteString.copyFrom(profileKey.serialize())) + .setJoinedAtRevision(5))); + } + private static ProfileKey newProfileKey() { try { return new ProfileKey(Util.getSecretBytes(32)); @@ -421,22 +451,26 @@ public final class GroupsV2Operations_decrypt_change_Test { } } + private ByteString encryptProfileKey(UUID uuid, ProfileKey profileKey) { + return ByteString.copyFrom(new ClientZkGroupCipher(groupSecretParams).encryptProfileKey(profileKey, uuid).serialize()); + } + static GroupCandidate groupCandidate(UUID uuid) { return new GroupCandidate(uuid, Optional.empty()); } GroupCandidate groupCandidate(UUID uuid, ProfileKey profileKey) { try { - ClientZkProfileOperations profileOperations = clientZkOperations.getProfileOperations(); - ProfileKeyCommitment commitment = profileKey.getCommitment(uuid); - ProfileKeyCredentialRequestContext requestContext = profileOperations.createProfileKeyCredentialRequestContext(uuid, profileKey); - ProfileKeyCredentialRequest request = requestContext.getRequest(); - ProfileKeyCredentialResponse profileKeyCredentialResponse = server.getProfileKeyCredentialResponse(request, uuid, commitment); - ProfileKeyCredential profileKeyCredential = profileOperations.receiveProfileKeyCredential(requestContext, profileKeyCredentialResponse); - GroupCandidate groupCandidate = new GroupCandidate(uuid, Optional.of(profileKeyCredential)); + ClientZkProfileOperations profileOperations = clientZkOperations.getProfileOperations(); + ProfileKeyCommitment commitment = profileKey.getCommitment(uuid); + ProfileKeyCredentialRequestContext requestContext = profileOperations.createProfileKeyCredentialRequestContext(uuid, profileKey); + ProfileKeyCredentialRequest request = requestContext.getRequest(); + ExpiringProfileKeyCredentialResponse expiringProfileKeyCredentialResponse = server.getExpiringProfileKeyCredentialResponse(request, uuid, commitment, Instant.now().plus(7, ChronoUnit.DAYS).truncatedTo(ChronoUnit.DAYS)); + ExpiringProfileKeyCredential profileKeyCredential = profileOperations.receiveExpiringProfileKeyCredential(requestContext, expiringProfileKeyCredentialResponse); + GroupCandidate groupCandidate = new GroupCandidate(uuid, Optional.of(profileKeyCredential)); ProfileKeyCredentialPresentation presentation = profileOperations.createProfileKeyCredentialPresentation(groupSecretParams, profileKeyCredential); - server.assertProfileKeyCredentialPresentation(groupSecretParams.getPublicParams(), presentation); + server.assertProfileKeyCredentialPresentation(groupSecretParams.getPublicParams(), presentation, Instant.now()); return groupCandidate; } catch (VerificationFailedException e) { @@ -447,9 +481,14 @@ public final class GroupsV2Operations_decrypt_change_Test { void assertDecryption(GroupChange.Actions.Builder inputChange, DecryptedGroupChange.Builder expectedDecrypted) { - UUID editor = UUID.randomUUID(); - GroupChange.Actions actions = inputChange.setSourceUuid(groupOperations.encryptUuid(editor)) - .build(); + UUID editor = UUID.randomUUID(); + assertDecryptionWithEditorSet(inputChange.setSourceUuid(groupOperations.encryptUuid(editor)), expectedDecrypted.setEditor(UuidUtil.toByteString(editor))); + } + + void assertDecryptionWithEditorSet(GroupChange.Actions.Builder inputChange, + DecryptedGroupChange.Builder expectedDecrypted) + { + GroupChange.Actions actions = inputChange.build(); GroupChange change = GroupChange.newBuilder() .setActions(actions.toByteString()) @@ -457,8 +496,7 @@ public final class GroupsV2Operations_decrypt_change_Test { DecryptedGroupChange decryptedGroupChange = decrypt(change); - assertEquals(expectedDecrypted.setEditor(UuidUtil.toByteString(editor)) - .build(), + assertEquals(expectedDecrypted.build(), decryptedGroupChange); } @@ -466,7 +504,7 @@ public final class GroupsV2Operations_decrypt_change_Test { try { return groupOperations.decryptChange(build, false).get(); } catch (InvalidProtocolBufferException | VerificationFailedException | InvalidGroupStateException e) { - throw new AssertionError(e); + throw new AssertionError(e); } } diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/ProtoTestUtils.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/ProtoTestUtils.java index d2dd3f810..7feece9c2 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/ProtoTestUtils.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/ProtoTestUtils.java @@ -55,6 +55,14 @@ final class ProtoTestUtils { return ByteString.copyFrom(concat); } + /** + * Emulates a presentation by concatenating the uuid and profile key which makes it suitable for + * equality assertions in these tests. + */ + static ByteString presentation(ByteString uuid, ByteString profileKey) { + return uuid.concat(profileKey); + } + static DecryptedModifyMemberRole promoteAdmin(UUID member) { return DecryptedModifyMemberRole.newBuilder() .setUuid(UuidUtil.toByteString(member)) @@ -148,6 +156,22 @@ final class ProtoTestUtils { return withProfileKey(member(uuid), profileKey); } + static DecryptedMember pendingPniAciMember(UUID uuid, UUID pni, ProfileKey profileKey) { + return DecryptedMember.newBuilder() + .setUuid(UuidUtil.toByteString(uuid)) + .setPni(UuidUtil.toByteString(pni)) + .setProfileKey(ByteString.copyFrom(profileKey.serialize())) + .build(); + } + + static DecryptedMember pendingPniAciMember(ByteString uuid, ByteString pni, ByteString profileKey) { + return DecryptedMember.newBuilder() + .setUuid(uuid) + .setPni(pni) + .setProfileKey(profileKey) + .build(); + } + static DecryptedMember admin(UUID uuid, ProfileKey profileKey) { return withProfileKey(admin(uuid), profileKey); } diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/TestZkGroupServer.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/TestZkGroupServer.java index 72288c767..a416a026b 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/TestZkGroupServer.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/TestZkGroupServer.java @@ -4,13 +4,14 @@ import org.signal.libsignal.zkgroup.ServerPublicParams; import org.signal.libsignal.zkgroup.ServerSecretParams; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.groups.GroupPublicParams; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialPresentation; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest; -import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialResponse; import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; import org.whispersystems.signalservice.testutil.LibSignalLibraryUtil; +import java.time.Instant; import java.util.UUID; /** @@ -34,13 +35,13 @@ final class TestZkGroupServer { return serverPublicParams; } - public ProfileKeyCredentialResponse getProfileKeyCredentialResponse(ProfileKeyCredentialRequest request, UUID uuid, ProfileKeyCommitment commitment) throws VerificationFailedException { - return serverZkProfileOperations.issueProfileKeyCredential(request, uuid, commitment); + public ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse(ProfileKeyCredentialRequest request, UUID uuid, ProfileKeyCommitment commitment, Instant expiration) throws VerificationFailedException { + return serverZkProfileOperations.issueExpiringProfileKeyCredential(request, uuid, commitment, expiration); } - public void assertProfileKeyCredentialPresentation(GroupPublicParams publicParams, ProfileKeyCredentialPresentation profileKeyCredentialPresentation) { + public void assertProfileKeyCredentialPresentation(GroupPublicParams publicParams, ProfileKeyCredentialPresentation profileKeyCredentialPresentation, Instant now) { try { - serverZkProfileOperations.verifyProfileKeyCredentialPresentation(publicParams, profileKeyCredentialPresentation); + serverZkProfileOperations.verifyProfileKeyCredentialPresentation(publicParams, profileKeyCredentialPresentation, now); } catch (VerificationFailedException e) { throw new AssertionError(e); } diff --git a/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt b/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt index f4ec0e680..25416a211 100644 --- a/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt +++ b/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt @@ -102,10 +102,10 @@ internal class SpinnerServer( database = dbName, databases = databases.keys.toList(), plugins = plugins.values.toList(), - tables = db.getTables().toTableInfo(), - indices = db.getIndexes().toIndexInfo(), - triggers = db.getTriggers().toTriggerInfo(), - queryResult = db.getTables().toQueryResult() + tables = db.getTables().use { it.toTableInfo() }, + indices = db.getIndexes().use { it.toIndexInfo() }, + triggers = db.getTriggers().use { it.toTriggerInfo() }, + queryResult = db.getTables().use { it.toQueryResult() } ) ) } @@ -141,7 +141,7 @@ internal class SpinnerServer( } val query = "select * from $table limit $pageSize offset ${pageSize * pageIndex}" - val queryResult = dbConfig.db.query(query).toQueryResult(columnTransformers = dbConfig.columnTransformers) + val queryResult = dbConfig.db.query(query).use { it.toQueryResult(columnTransformers = dbConfig.columnTransformers) } return renderTemplate( "browse", @@ -217,7 +217,7 @@ internal class SpinnerServer( databases = databases.keys.toList(), plugins = plugins.values.toList(), query = rawQuery, - queryResult = dbConfig.db.query(query).toQueryResult(queryStartTime = startTime, columnTransformers = dbConfig.columnTransformers) + queryResult = dbConfig.db.query(query).use { it.toQueryResult(queryStartTime = startTime, columnTransformers = dbConfig.columnTransformers) } ) ) }