feat: add revoke and unrevoke subaccount for removing users
This commit is contained in:
parent
d02230cee3
commit
253788b4ed
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue