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 membersConfig = configFactory.getGroupMemberConfig(sessionId) ?: return
|
||||||
val infoConfig = configFactory.getGroupInfoConfig(sessionId) ?: return
|
val infoConfig = configFactory.getGroupInfoConfig(sessionId) ?: return
|
||||||
|
|
||||||
|
// Filter out people who aren't already invited
|
||||||
val filteredMembers = invitees.filter {
|
val filteredMembers = invitees.filter {
|
||||||
membersConfig.get(it) == null
|
membersConfig.get(it) == null
|
||||||
}
|
}
|
||||||
|
// Create each member's contact info if we have it
|
||||||
filteredMembers.forEach { memberSessionId ->
|
filteredMembers.forEach { memberSessionId ->
|
||||||
val contact = getContactWithSessionID(memberSessionId)
|
val contact = getContactWithSessionID(memberSessionId)
|
||||||
val name = contact?.name
|
val name = contact?.name
|
||||||
|
@ -1310,6 +1312,7 @@ open class Storage(
|
||||||
membersConfig.set(member)
|
membersConfig.set(member)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// re-key for new members
|
||||||
val keysConfig = configFactory.getGroupKeysConfig(
|
val keysConfig = configFactory.getGroupKeysConfig(
|
||||||
sessionId,
|
sessionId,
|
||||||
info = infoConfig,
|
info = infoConfig,
|
||||||
|
@ -1321,6 +1324,14 @@ open class Storage(
|
||||||
|
|
||||||
val sentTimestamp = SnodeAPI.nowWithOffset
|
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(
|
val message = SnodeMessage(
|
||||||
groupSessionId,
|
groupSessionId,
|
||||||
Base64.encodeBytes(keysConfig.pendingConfig()!!), // should not be null from checking has pending
|
Base64.encodeBytes(keysConfig.pendingConfig()!!), // should not be null from checking has pending
|
||||||
|
@ -1337,7 +1348,8 @@ open class Storage(
|
||||||
SnodeAPI.getRawBatchResponse(
|
SnodeAPI.getRawBatchResponse(
|
||||||
snode,
|
snode,
|
||||||
groupSessionId,
|
groupSessionId,
|
||||||
listOf(authenticatedBatch),
|
listOf(unrevocation, authenticatedBatch),
|
||||||
|
sequence = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1345,6 +1357,7 @@ open class Storage(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response.get()
|
response.get()
|
||||||
|
// todo: error handling here
|
||||||
|
|
||||||
val newConfigSync = ConfigurationSyncJob(destination)
|
val newConfigSync = ConfigurationSyncJob(destination)
|
||||||
var exception: Exception? = null
|
var exception: Exception? = null
|
||||||
|
@ -1441,7 +1454,7 @@ open class Storage(
|
||||||
members.set(promoted)
|
members.set(promoted)
|
||||||
|
|
||||||
val message = GroupUpdated(
|
val message = GroupUpdated(
|
||||||
DataMessage.GroupUpdateMessage.newBuilder()
|
GroupUpdateMessage.newBuilder()
|
||||||
.setPromoteMessage(
|
.setPromoteMessage(
|
||||||
DataMessage.GroupUpdatePromoteMessage.newBuilder()
|
DataMessage.GroupUpdatePromoteMessage.newBuilder()
|
||||||
.setGroupIdentitySeed(ByteString.copyFrom(adminKey))
|
.setGroupIdentitySeed(ByteString.copyFrom(adminKey))
|
||||||
|
@ -1460,7 +1473,7 @@ open class Storage(
|
||||||
val messageToSign = "MEMBER_CHANGE${GroupUpdateMemberChangeMessage.Type.PROMOTED.name}$timestamp"
|
val messageToSign = "MEMBER_CHANGE${GroupUpdateMemberChangeMessage.Type.PROMOTED.name}$timestamp"
|
||||||
val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey)
|
val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey)
|
||||||
val message = GroupUpdated(
|
val message = GroupUpdated(
|
||||||
DataMessage.GroupUpdateMessage.newBuilder()
|
GroupUpdateMessage.newBuilder()
|
||||||
.setMemberChangeMessage(
|
.setMemberChangeMessage(
|
||||||
GroupUpdateMemberChangeMessage.newBuilder()
|
GroupUpdateMemberChangeMessage.newBuilder()
|
||||||
.addAllMemberSessionIds(promotions.toList())
|
.addAllMemberSessionIds(promotions.toList())
|
||||||
|
@ -1475,6 +1488,108 @@ open class Storage(
|
||||||
insertGroupInfoChange(message, closedGroupId)
|
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) {
|
override fun handlePromoted(keyPair: KeyPair) {
|
||||||
val closedGroupId = SessionId(IdPrefix.GROUP, keyPair.pubKey)
|
val closedGroupId = SessionId(IdPrefix.GROUP, keyPair.pubKey)
|
||||||
val ourSessionId = getUserPublicKey()!!
|
val ourSessionId = getUserPublicKey()!!
|
||||||
|
@ -1484,6 +1599,7 @@ open class Storage(
|
||||||
|
|
||||||
val modified = closedGroup.copy(adminKey = keyPair.secretKey, authData = byteArrayOf())
|
val modified = closedGroup.copy(adminKey = keyPair.secretKey, authData = byteArrayOf())
|
||||||
userGroups.set(modified)
|
userGroups.set(modified)
|
||||||
|
configFactory.scheduleUpdate(Destination.from(fromSerialized(getUserPublicKey()!!)))
|
||||||
val info = configFactory.getGroupInfoConfig(closedGroupId) ?: return
|
val info = configFactory.getGroupInfoConfig(closedGroupId) ?: return
|
||||||
val members = configFactory.getGroupMemberConfig(closedGroupId) ?: return
|
val members = configFactory.getGroupMemberConfig(closedGroupId) ?: return
|
||||||
val keys = configFactory.getGroupKeysConfig(closedGroupId, info, members, free = false) ?: return
|
val keys = configFactory.getGroupKeysConfig(closedGroupId, info, members, free = false) ?: return
|
||||||
|
|
|
@ -97,6 +97,9 @@ fun EditClosedGroupScreen(
|
||||||
onPromote = { contact ->
|
onPromote = { contact ->
|
||||||
eventSink(EditGroupEvent.PromoteContact(contact))
|
eventSink(EditGroupEvent.PromoteContact(contact))
|
||||||
},
|
},
|
||||||
|
onRemove = { contact ->
|
||||||
|
eventSink(EditGroupEvent.RemoveContact(contact))
|
||||||
|
},
|
||||||
viewState = viewState
|
viewState = viewState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -192,6 +195,9 @@ class EditGroupViewModel @AssistedInject constructor(
|
||||||
// do a buffer
|
// do a buffer
|
||||||
storage.promoteMember(groupSessionId, arrayOf(event.contactSessionId))
|
storage.promoteMember(groupSessionId, arrayOf(event.contactSessionId))
|
||||||
}
|
}
|
||||||
|
is EditGroupEvent.RemoveContact -> {
|
||||||
|
storage.removeMember(groupSessionId, arrayOf(event.contactSessionId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -247,6 +253,7 @@ fun EditGroupView(
|
||||||
onInvite: ()->Unit,
|
onInvite: ()->Unit,
|
||||||
onReinvite: (String)->Unit,
|
onReinvite: (String)->Unit,
|
||||||
onPromote: (String)->Unit,
|
onPromote: (String)->Unit,
|
||||||
|
onRemove: (String)->Unit,
|
||||||
viewState: EditGroupViewState,
|
viewState: EditGroupViewState,
|
||||||
) {
|
) {
|
||||||
val scaffoldState = rememberScaffoldState()
|
val scaffoldState = rememberScaffoldState()
|
||||||
|
@ -337,9 +344,11 @@ fun EditGroupView(
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(
|
||||||
Color(
|
Color(
|
||||||
MaterialColors.getColor(LocalContext.current,
|
MaterialColors.getColor(
|
||||||
|
LocalContext.current,
|
||||||
R.attr.colorControlHighlight,
|
R.attr.colorControlHighlight,
|
||||||
MaterialTheme.colors.onPrimary.toArgb())
|
MaterialTheme.colors.onPrimary.toArgb()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
@ -357,9 +366,11 @@ fun EditGroupView(
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(
|
||||||
Color(
|
Color(
|
||||||
MaterialColors.getColor(LocalContext.current,
|
MaterialColors.getColor(
|
||||||
|
LocalContext.current,
|
||||||
R.attr.colorControlHighlight,
|
R.attr.colorControlHighlight,
|
||||||
MaterialTheme.colors.onPrimary.toArgb())
|
MaterialTheme.colors.onPrimary.toArgb()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
@ -368,7 +379,24 @@ fun EditGroupView(
|
||||||
color = MaterialTheme.colors.onPrimary
|
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()
|
val contacts: ContactList): EditGroupEvent()
|
||||||
data class ReInviteContact(val contactSessionId: String): EditGroupEvent()
|
data class ReInviteContact(val contactSessionId: String): EditGroupEvent()
|
||||||
data class PromoteContact(val contactSessionId: String): EditGroupEvent()
|
data class PromoteContact(val contactSessionId: String): EditGroupEvent()
|
||||||
|
data class RemoveContact(val contactSessionId: String): EditGroupEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class EditGroupInviteViewState(
|
data class EditGroupInviteViewState(
|
||||||
|
@ -443,16 +472,22 @@ fun PreviewList() {
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
val twoMember = MemberViewModel(
|
val twoMember = MemberViewModel(
|
||||||
"Test User",
|
"Test User 2",
|
||||||
"05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235",
|
"05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235",
|
||||||
MemberState.InviteFailed,
|
MemberState.InviteFailed,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
val threeMember = MemberViewModel(
|
||||||
|
"Test User 3",
|
||||||
|
"05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236",
|
||||||
|
MemberState.Member,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
val viewState = EditGroupViewState(
|
val viewState = EditGroupViewState(
|
||||||
"Preview",
|
"Preview",
|
||||||
"This is a preview description",
|
"This is a preview description",
|
||||||
listOf(oneMember, twoMember),
|
listOf(oneMember, twoMember, threeMember),
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -462,6 +497,7 @@ fun PreviewList() {
|
||||||
onInvite = {},
|
onInvite = {},
|
||||||
onReinvite = {},
|
onReinvite = {},
|
||||||
onPromote = {},
|
onPromote = {},
|
||||||
|
onRemove = {},
|
||||||
viewState = viewState
|
viewState = viewState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -247,6 +247,21 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_makeSubAccount(JNIE
|
||||||
return jbytes;
|
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"
|
extern "C"
|
||||||
JNIEXPORT jobject JNICALL
|
JNIEXPORT jobject JNICALL
|
||||||
Java_network_loki_messenger_libsession_1util_GroupKeysConfig_subAccountSign(JNIEnv *env,
|
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"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
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) {
|
jobject group_member) {
|
||||||
auto config = ptrToMembers(env, thiz);
|
auto config = ptrToMembers(env, thiz);
|
||||||
auto member = util::deserialize_group_member(env, group_member);
|
auto member = util::deserialize_group_member(env, group_member);
|
||||||
return config->erase(member.session_id);
|
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"
|
extern "C"
|
||||||
JNIEXPORT jobject JNICALL
|
JNIEXPORT jobject JNICALL
|
||||||
Java_network_loki_messenger_libsession_1util_GroupMembersConfig_get(JNIEnv *env, jobject thiz,
|
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 all(): Stack<GroupMember>
|
||||||
external fun erase(groupMember: GroupMember): Boolean
|
external fun erase(groupMember: GroupMember): Boolean
|
||||||
|
external fun erase(pubKeyHex: String): Boolean
|
||||||
external fun get(pubKeyHex: String): GroupMember?
|
external fun get(pubKeyHex: String): GroupMember?
|
||||||
external fun getOrConstruct(pubKeyHex: String): GroupMember
|
external fun getOrConstruct(pubKeyHex: String): GroupMember
|
||||||
external fun set(groupMember: GroupMember)
|
external fun set(groupMember: GroupMember)
|
||||||
|
@ -335,6 +336,7 @@ class GroupKeysConfig(pointer: Long): ConfigSig(pointer) {
|
||||||
external fun keys(): Stack<ByteArray>
|
external fun keys(): Stack<ByteArray>
|
||||||
|
|
||||||
external fun makeSubAccount(sessionId: SessionId, canWrite: Boolean = true, canDelete: Boolean = false): 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
|
external fun subAccountSign(message: ByteArray, signingValue: ByteArray): SwarmAuth
|
||||||
|
|
||||||
|
|
|
@ -170,6 +170,7 @@ interface StorageProtocol {
|
||||||
fun inviteClosedGroupMembers(groupSessionId: String, invitees: List<String>)
|
fun inviteClosedGroupMembers(groupSessionId: String, invitees: List<String>)
|
||||||
fun insertGroupInfoChange(message: GroupUpdated, closedGroup: SessionId)
|
fun insertGroupInfoChange(message: GroupUpdated, closedGroup: SessionId)
|
||||||
fun promoteMember(groupSessionId: String, promotions: Array<String>)
|
fun promoteMember(groupSessionId: String, promotions: Array<String>)
|
||||||
|
fun removeMember(groupSessionId: String, removedMembers: Array<String>, fromDelete: Boolean = false)
|
||||||
fun handlePromoted(keyPair: KeyPair)
|
fun handlePromoted(keyPair: KeyPair)
|
||||||
fun leaveGroup(groupSessionId: String)
|
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 {
|
fun getRawBatchResponse(snode: Snode, publicKey: String, requests: List<SnodeBatchRequestInfo>, sequence: Boolean = false): RawResponsePromise {
|
||||||
val parameters = mutableMapOf<String, Any>(
|
val parameters = mutableMapOf<String, Any>(
|
||||||
|
@ -672,6 +704,70 @@ object SnodeAPI {
|
||||||
return params
|
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 {
|
fun getMessages(publicKey: String): MessageListPromise {
|
||||||
return retryIfNeeded(maxRetryCount) {
|
return retryIfNeeded(maxRetryCount) {
|
||||||
getSingleTargetSnode(publicKey).bind { snode ->
|
getSingleTargetSnode(publicKey).bind { snode ->
|
||||||
|
|
|
@ -14,7 +14,9 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) {
|
||||||
Batch("batch"),
|
Batch("batch"),
|
||||||
Sequence("sequence"),
|
Sequence("sequence"),
|
||||||
Expire("expire"),
|
Expire("expire"),
|
||||||
GetExpiries("get_expiries")
|
GetExpiries("get_expiries"),
|
||||||
|
RevokeSubAccount("revoke_subaccount"),
|
||||||
|
UnrevokeSubAccount("unrevoke_subaccount"),
|
||||||
}
|
}
|
||||||
|
|
||||||
data class KeySet(val ed25519Key: String, val x25519Key: String)
|
data class KeySet(val ed25519Key: String, val x25519Key: String)
|
||||||
|
|
Loading…
Reference in New Issue