feat: add revoke and unrevoke subaccount for removing users

This commit is contained in:
0x330a 2023-11-30 12:02:28 +11:00
parent d02230cee3
commit 253788b4ed
8 changed files with 290 additions and 12 deletions

View File

@ -1291,9 +1291,11 @@ open class Storage(
val membersConfig = configFactory.getGroupMemberConfig(sessionId) ?: return
val infoConfig = configFactory.getGroupInfoConfig(sessionId) ?: return
// Filter out people who aren't already invited
val filteredMembers = invitees.filter {
membersConfig.get(it) == null
}
// Create each member's contact info if we have it
filteredMembers.forEach { memberSessionId ->
val contact = getContactWithSessionID(memberSessionId)
val name = contact?.name
@ -1310,6 +1312,7 @@ open class Storage(
membersConfig.set(member)
}
// re-key for new members
val keysConfig = configFactory.getGroupKeysConfig(
sessionId,
info = infoConfig,
@ -1321,6 +1324,14 @@ open class Storage(
val sentTimestamp = SnodeAPI.nowWithOffset
// build unrevocation, in case of re-adding members
val unrevocation = SnodeAPI.buildAuthenticatedUnrevokeSubKeyBatchRequest(
groupSessionId,
adminKey,
filteredMembers.map { keysConfig.getSubAccountToken(SessionId.from(it)) }.toTypedArray()
) ?: return Log.e("ClosedGroup", "Failed to build revocation update")
// Build and store the key update in group swarm
val message = SnodeMessage(
groupSessionId,
Base64.encodeBytes(keysConfig.pendingConfig()!!), // should not be null from checking has pending
@ -1337,7 +1348,8 @@ open class Storage(
SnodeAPI.getRawBatchResponse(
snode,
groupSessionId,
listOf(authenticatedBatch),
listOf(unrevocation, authenticatedBatch),
sequence = true
)
}
@ -1345,6 +1357,7 @@ open class Storage(
try {
response.get()
// todo: error handling here
val newConfigSync = ConfigurationSyncJob(destination)
var exception: Exception? = null
@ -1441,7 +1454,7 @@ open class Storage(
members.set(promoted)
val message = GroupUpdated(
DataMessage.GroupUpdateMessage.newBuilder()
GroupUpdateMessage.newBuilder()
.setPromoteMessage(
DataMessage.GroupUpdatePromoteMessage.newBuilder()
.setGroupIdentitySeed(ByteString.copyFrom(adminKey))
@ -1460,7 +1473,7 @@ open class Storage(
val messageToSign = "MEMBER_CHANGE${GroupUpdateMemberChangeMessage.Type.PROMOTED.name}$timestamp"
val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey)
val message = GroupUpdated(
DataMessage.GroupUpdateMessage.newBuilder()
GroupUpdateMessage.newBuilder()
.setMemberChangeMessage(
GroupUpdateMemberChangeMessage.newBuilder()
.addAllMemberSessionIds(promotions.toList())
@ -1475,6 +1488,108 @@ open class Storage(
insertGroupInfoChange(message, closedGroupId)
}
override fun removeMember(groupSessionId: String, removedMembers: Array<String>, fromDelete: Boolean) {
val closedGroupId = SessionId.from(groupSessionId)
val adminKey = configFactory.userGroups?.getClosedGroup(groupSessionId)?.adminKey ?: return
if (adminKey.isEmpty()) {
return Log.e("ClosedGroup", "No admin key for group")
}
val info = configFactory.getGroupInfoConfig(closedGroupId) ?: return
val members = configFactory.getGroupMemberConfig(closedGroupId) ?: return
val keys = configFactory.getGroupKeysConfig(closedGroupId, info, members, free = false) ?: return
removedMembers.forEach { sessionId ->
members.erase(sessionId)
}
// Re-key for removed members
keys.rekey(info, members)
val revocation = SnodeAPI.buildAuthenticatedRevokeSubKeyBatchRequest(
groupSessionId,
adminKey,
removedMembers.map { keys.getSubAccountToken(SessionId.from(it)) }.toTypedArray()
) ?: return Log.e("ClosedGroup", "Failed to build revocation update")
// Build and store the key update in group swarm
val storeKeyMessage = SnodeMessage(
groupSessionId,
Base64.encodeBytes(keys.pendingConfig()!!), // should not be null from checking has pending
SnodeMessage.CONFIG_TTL,
SnodeAPI.nowWithOffset
)
val authenticatedBatch = SnodeAPI.buildAuthenticatedStoreBatchInfo(
keys.namespace(),
storeKeyMessage,
adminKey
)
val response = SnodeAPI.getSingleTargetSnode(groupSessionId).bind { snode ->
SnodeAPI.getRawBatchResponse(
snode,
groupSessionId,
listOf(revocation, authenticatedBatch),
sequence = true
)
}
try {
// handle new key update and revocations response
val rawResponse = response.get()
val results = (rawResponse["results"] as ArrayList<Any>).first() as Map<String,Any>
if (results["code"] as Int != 200) {
throw Exception("Response wasn't successful for revoke and key update: ${results["body"] as? String}")
}
val newConfigSync = ConfigurationSyncJob(Destination.ClosedGroup(groupSessionId))
configFactory.saveGroupConfigs(keys, info, members)
info.free()
members.free()
keys.free()
var exception: Exception? = null
val delegate = object: JobDelegate {
override fun handleJobSucceeded(job: Job, dispatcherName: String) {}
override fun handleJobFailed(job: Job, dispatcherName: String, error: Exception) { exception = error }
override fun handleJobFailedPermanently(
job: Job,
dispatcherName: String,
error: Exception
) { exception = error }
}
newConfigSync.delegate = delegate
runBlocking {
newConfigSync.execute("updating-members")
}
// rethrow failure
exception?.let { throw it }
val timestamp = SnodeAPI.nowWithOffset
val messageToSign = "MEMBER_CHANGE${GroupUpdateMemberChangeMessage.Type.REMOVED.name}$timestamp"
val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey)
val updateMessage = GroupUpdateMessage.newBuilder()
.setMemberChangeMessage(
GroupUpdateMemberChangeMessage.newBuilder()
.addAllMemberSessionIds(removedMembers.toList())
.setType(GroupUpdateMemberChangeMessage.Type.REMOVED)
.setAdminSignature(ByteString.copyFrom(signature))
)
.build()
val message = GroupUpdated(
updateMessage
).apply { sentTimestamp = timestamp }
val groupDestination = Destination.ClosedGroup(groupSessionId)
MessageSender.send(message, groupDestination, false)
insertGroupInfoChange(message, closedGroupId)
} catch (e: Exception) {
info.free()
members.free()
keys.free()
}
}
override fun handlePromoted(keyPair: KeyPair) {
val closedGroupId = SessionId(IdPrefix.GROUP, keyPair.pubKey)
val ourSessionId = getUserPublicKey()!!
@ -1484,6 +1599,7 @@ open class Storage(
val modified = closedGroup.copy(adminKey = keyPair.secretKey, authData = byteArrayOf())
userGroups.set(modified)
configFactory.scheduleUpdate(Destination.from(fromSerialized(getUserPublicKey()!!)))
val info = configFactory.getGroupInfoConfig(closedGroupId) ?: return
val members = configFactory.getGroupMemberConfig(closedGroupId) ?: return
val keys = configFactory.getGroupKeysConfig(closedGroupId, info, members, free = false) ?: return

View File

@ -97,6 +97,9 @@ fun EditClosedGroupScreen(
onPromote = { contact ->
eventSink(EditGroupEvent.PromoteContact(contact))
},
onRemove = { contact ->
eventSink(EditGroupEvent.RemoveContact(contact))
},
viewState = viewState
)
}
@ -192,6 +195,9 @@ class EditGroupViewModel @AssistedInject constructor(
// do a buffer
storage.promoteMember(groupSessionId, arrayOf(event.contactSessionId))
}
is EditGroupEvent.RemoveContact -> {
storage.removeMember(groupSessionId, arrayOf(event.contactSessionId))
}
}
}
}
@ -247,6 +253,7 @@ fun EditGroupView(
onInvite: ()->Unit,
onReinvite: (String)->Unit,
onPromote: (String)->Unit,
onRemove: (String)->Unit,
viewState: EditGroupViewState,
) {
val scaffoldState = rememberScaffoldState()
@ -337,9 +344,11 @@ fun EditGroupView(
.clip(CircleShape)
.background(
Color(
MaterialColors.getColor(LocalContext.current,
MaterialColors.getColor(
LocalContext.current,
R.attr.colorControlHighlight,
MaterialTheme.colors.onPrimary.toArgb())
MaterialTheme.colors.onPrimary.toArgb()
)
)
)
) {
@ -357,9 +366,11 @@ fun EditGroupView(
.clip(CircleShape)
.background(
Color(
MaterialColors.getColor(LocalContext.current,
MaterialColors.getColor(
LocalContext.current,
R.attr.colorControlHighlight,
MaterialTheme.colors.onPrimary.toArgb())
MaterialTheme.colors.onPrimary.toArgb()
)
)
)
) {
@ -368,7 +379,24 @@ fun EditGroupView(
color = MaterialTheme.colors.onPrimary
)
}
TextButton(
onClick = {
onRemove(member.memberSessionId)
},
modifier = Modifier
.clip(CircleShape)
.background(
Color(
MaterialColors.getColor(
LocalContext.current,
R.attr.colorControlHighlight,
MaterialTheme.colors.onPrimary.toArgb()
)
)
)
) {
Icon(painter = painterResource(id = R.drawable.ic_baseline_close_24), contentDescription = null)
}
}
}
}
@ -425,6 +453,7 @@ sealed class EditGroupEvent {
val contacts: ContactList): EditGroupEvent()
data class ReInviteContact(val contactSessionId: String): EditGroupEvent()
data class PromoteContact(val contactSessionId: String): EditGroupEvent()
data class RemoveContact(val contactSessionId: String): EditGroupEvent()
}
data class EditGroupInviteViewState(
@ -443,16 +472,22 @@ fun PreviewList() {
false
)
val twoMember = MemberViewModel(
"Test User",
"Test User 2",
"05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235",
MemberState.InviteFailed,
false
)
val threeMember = MemberViewModel(
"Test User 3",
"05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236",
MemberState.Member,
false
)
val viewState = EditGroupViewState(
"Preview",
"This is a preview description",
listOf(oneMember, twoMember),
listOf(oneMember, twoMember, threeMember),
true
)
@ -462,6 +497,7 @@ fun PreviewList() {
onInvite = {},
onReinvite = {},
onPromote = {},
onRemove = {},
viewState = viewState
)
}

View File

@ -247,6 +247,21 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_makeSubAccount(JNIE
return jbytes;
}
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_network_loki_messenger_libsession_1util_GroupKeysConfig_getSubAccountToken(JNIEnv *env,
jobject thiz,
jobject session_id,
jboolean can_write,
jboolean can_delete) {
std::lock_guard lock{util::util_mutex_};
auto ptr = ptrToKeys(env, thiz);
auto deserialized_id = util::deserialize_session_id(env, session_id);
auto token = ptr->swarm_subaccount_token(deserialized_id, can_write, can_delete);
auto jbytes = util::bytes_from_ustring(env, token);
return jbytes;
}
extern "C"
JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_GroupKeysConfig_subAccountSign(JNIEnv *env,

View File

@ -45,13 +45,23 @@ Java_network_loki_messenger_libsession_1util_GroupMembersConfig_all(JNIEnv *env,
extern "C"
JNIEXPORT jboolean JNICALL
Java_network_loki_messenger_libsession_1util_GroupMembersConfig_erase(JNIEnv *env, jobject thiz,
Java_network_loki_messenger_libsession_1util_GroupMembersConfig_erase__Lnetwork_loki_messenger_libsession_1util_util_GroupMember_2(JNIEnv *env, jobject thiz,
jobject group_member) {
auto config = ptrToMembers(env, thiz);
auto member = util::deserialize_group_member(env, group_member);
return config->erase(member.session_id);
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_network_loki_messenger_libsession_1util_GroupMembersConfig_erase__Ljava_lang_String_2(JNIEnv *env, jobject thiz, jstring pub_key_hex) {
auto config = ptrToMembers(env, thiz);
auto member_id = env->GetStringUTFChars(pub_key_hex, nullptr);
auto erased = config->erase(member_id);
env->ReleaseStringUTFChars(pub_key_hex, member_id);
return erased;
}
extern "C"
JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_GroupMembersConfig_get(JNIEnv *env, jobject thiz,

View File

@ -284,6 +284,7 @@ class GroupMembersConfig(pointer: Long): ConfigBase(pointer), Closeable {
external fun all(): Stack<GroupMember>
external fun erase(groupMember: GroupMember): Boolean
external fun erase(pubKeyHex: String): Boolean
external fun get(pubKeyHex: String): GroupMember?
external fun getOrConstruct(pubKeyHex: String): GroupMember
external fun set(groupMember: GroupMember)
@ -335,6 +336,7 @@ class GroupKeysConfig(pointer: Long): ConfigSig(pointer) {
external fun keys(): Stack<ByteArray>
external fun makeSubAccount(sessionId: SessionId, canWrite: Boolean = true, canDelete: Boolean = false): ByteArray
external fun getSubAccountToken(sessionId: SessionId, canWrite: Boolean = true, canDelete: Boolean = false): ByteArray
external fun subAccountSign(message: ByteArray, signingValue: ByteArray): SwarmAuth

View File

@ -170,6 +170,7 @@ interface StorageProtocol {
fun inviteClosedGroupMembers(groupSessionId: String, invitees: List<String>)
fun insertGroupInfoChange(message: GroupUpdated, closedGroup: SessionId)
fun promoteMember(groupSessionId: String, promotions: Array<String>)
fun removeMember(groupSessionId: String, removedMembers: Array<String>, fromDelete: Boolean = false)
fun handlePromoted(keyPair: KeyPair)
fun leaveGroup(groupSessionId: String)

View File

@ -553,7 +553,39 @@ object SnodeAPI {
)
}
fun buildAuthenticatedUnrevokeSubKeyBatchRequest(
publicKeyDestination: String,
signingKey: ByteArray,
subAccounts: Array<ByteArray>,
): SnodeBatchRequestInfo? {
val params= buildUnrevokeAccountParams(
publicKeyDestination,
signingKey,
subAccounts
) ?: return null
return SnodeBatchRequestInfo(
Snode.Method.UnrevokeSubAccount.rawValue,
params,
null
)
}
fun buildAuthenticatedRevokeSubKeyBatchRequest(
publicKeyDestination: String,
signingKey: ByteArray,
subAccounts: Array<ByteArray>,
): SnodeBatchRequestInfo? {
val params = buildRevokeAccountParams(
publicKeyDestination,
signingKey,
subAccounts
) ?: return null
return SnodeBatchRequestInfo(
Snode.Method.RevokeSubAccount.rawValue,
params,
null
)
}
fun getRawBatchResponse(snode: Snode, publicKey: String, requests: List<SnodeBatchRequestInfo>, sequence: Boolean = false): RawResponsePromise {
val parameters = mutableMapOf<String, Any>(
@ -672,6 +704,70 @@ object SnodeAPI {
return params
}
private fun buildUnrevokeAccountParams(
publicKey: String,
signingKey: ByteArray,
unrevoke: Array<ByteArray>,
pubKeyEd25519: String? = null,
): Map<String, Any>? {
val timestamp = nowWithOffset
val params = mutableMapOf(
"pubkey" to publicKey,
"timestamp" to timestamp,
"unrevoke" to unrevoke.map { Base64.encodeBytes(it) }
)
val signData = "unrevoke_subaccount$timestamp".toByteArray() + unrevoke.reduce(ByteArray::plus)
val signature = ByteArray(Sign.BYTES)
try {
sodium.cryptoSignDetached(
signature,
signData,
signData.size.toLong(),
signingKey
)
} catch (e: Exception) {
Log.e("Loki", "Signing data failed with secret key", e)
return null
}
params["signature"] = Base64.encodeBytes(signature)
if (pubKeyEd25519 != null) {
params["pubkey_ed25519"] = pubKeyEd25519
}
return params
}
private fun buildRevokeAccountParams(
publicKey: String,
signingKey: ByteArray,
revoke: Array<ByteArray>,
pubKeyEd25519: String? = null,
): Map<String,Any>? {
val timestamp = nowWithOffset
val params = mutableMapOf(
"pubkey" to publicKey,
"timestamp" to timestamp,
"revoke" to revoke.map { Base64.encodeBytes(it) },
)
val signData = "revoke_subaccount$timestamp".toByteArray() + revoke.reduce(ByteArray::plus)
val signature = ByteArray(Sign.BYTES)
try {
sodium.cryptoSignDetached(
signature,
signData,
signData.size.toLong(),
signingKey
)
} catch (e: Exception) {
Log.e("Loki", "Signing data failed with secret key", e)
return null
}
params["signature"] = Base64.encodeBytes(signature)
if (pubKeyEd25519 != null) {
params["pubkey_ed25519"] = pubKeyEd25519
}
return params
}
fun getMessages(publicKey: String): MessageListPromise {
return retryIfNeeded(maxRetryCount) {
getSingleTargetSnode(publicKey).bind { snode ->

View File

@ -14,7 +14,9 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) {
Batch("batch"),
Sequence("sequence"),
Expire("expire"),
GetExpiries("get_expiries")
GetExpiries("get_expiries"),
RevokeSubAccount("revoke_subaccount"),
UnrevokeSubAccount("unrevoke_subaccount"),
}
data class KeySet(val ed25519Key: String, val x25519Key: String)