Added the updated revoke & unrevoke endpoints

Added the updated revoke & unrevoke endpoints when removing and adding group members
Updated the group creation to upload the provided image and set it to the group correctly
pull/941/head
Morgan Pretty 2 years ago
parent 7e34c1609f
commit dffe321bc4

@ -332,7 +332,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
ModalActivityIndicatorViewController.present(fromViewController: navigationController!) { [weak self] _ in
MessageSender
// .createLegacyClosedGroup(name: name, members: selectedProfiles.map { $0.0 }.asSet())
.createGroup(name: name, description: nil, displayPicture: nil, members: selectedProfiles)
.createGroup(name: name, description: nil, displayPictureData: nil, members: selectedProfiles)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main)
.sinkUntilComplete(

@ -20,34 +20,42 @@ extension MessageSender {
public static func createGroup(
name: String,
description: String?,
displayPicture: SignalAttachment?,
displayPictureData: Data?,
members: [(String, Profile?)],
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<SessionThread, Error> {
typealias ImageUploadResponse = (downloadUrl: String, fileName: String, encryptionKey: Data)
return Just(())
.setFailureType(to: Error.self)
.flatMap { _ -> AnyPublisher<(url: String, filename: String, encryptionKey: Data)?, Error> in
guard let displayPicture: SignalAttachment = displayPicture else {
.flatMap { _ -> AnyPublisher<ImageUploadResponse?, Error> in
guard let displayPictureData: Data = displayPictureData else {
return Just(nil)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
// TODO: Upload group image first
return Just(nil)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
return Deferred {
Future<ImageUploadResponse?, Error> { resolver in
DisplayPictureManager.prepareAndUploadDisplayPicture(
queue: DispatchQueue.global(qos: .userInitiated),
imageData: displayPictureData,
success: { resolver(Result.success($0)) },
failure: { resolver(Result.failure($0)) },
using: dependencies
)
}
}.eraseToAnyPublisher()
}
.flatMap { displayPictureInfo -> AnyPublisher<PreparedGroupData, Error> in
dependencies[singleton: .storage].writePublisher(using: dependencies) { db -> PreparedGroupData in
// Create and cache the libSession entries
let userSessionId: SessionId = getUserSessionId(db, using: dependencies)
let currentUserProfile: Profile = Profile.fetchOrCreateCurrentUser(db, using: dependencies)
let createdInfo: SessionUtil.CreatedGroupInfo = try SessionUtil.createGroup(
db,
name: name,
description: description,
displayPictureUrl: displayPictureInfo?.url,
displayPictureFilename: displayPictureInfo?.filename,
displayPictureUrl: displayPictureInfo?.downloadUrl,
displayPictureFilename: displayPictureInfo?.fileName,
displayPictureEncryptionKey: displayPictureInfo?.encryptionKey,
members: members,
using: dependencies
@ -402,9 +410,14 @@ extension MessageSender {
}
/// Generate the data needed to send the new members invitations to the group
let memberJobData: [(id: String, profile: Profile?, jobDetails: GroupInviteMemberJob.Details)] = try members
let memberJobData: [(id: String, profile: Profile?, jobDetails: GroupInviteMemberJob.Details, subaccountToken: [UInt8])] = try members
.map { id, profile in
// Generate authData for the newly added member
let subaccountToken: [UInt8] = try SessionUtil.generateSubaccountToken(
groupSessionId: sessionId,
memberId: id,
using: dependencies
)
let memberAuthInfo: Authentication.Info = try SessionUtil.generateAuthData(
groupSessionId: sessionId,
memberId: id,
@ -415,7 +428,7 @@ extension MessageSender {
authInfo: memberAuthInfo
)
return (id, profile, inviteDetails)
return (id, profile, inviteDetails, subaccountToken)
}
/// Unrevoke the newly added members just in case they had previously gotten their access to the group
@ -427,9 +440,9 @@ extension MessageSender {
try? SnodeAPI
.preparedBatch(
db,
requests: try memberJobDataChunk.map { id, _, jobDetails in
requests: try memberJobDataChunk.map { id, _, _, subaccountToken in
try SnodeAPI.preparedUnrevokeSubaccount(
subaccountToUnrevoke: jobDetails.memberAuthData.toHexString(),
subaccountToUnrevoke: subaccountToken,
authMethod: Authentication.groupAdmin(
groupSessionId: sessionId,
ed25519SecretKey: Array(groupIdentityPrivateKey)
@ -447,7 +460,7 @@ extension MessageSender {
}
/// Make the required changes for each added member
try memberJobData.forEach { id, profile, inviteJobDetails in
try memberJobData.forEach { id, profile, inviteJobDetails, _ in
/// Add the member to the database
try GroupMember(
groupId: sessionId.hexString,
@ -518,6 +531,11 @@ extension MessageSender {
.fetchOne(db)
else { throw MessageSenderError.invalidClosedGroupUpdate }
let subaccountToken: [UInt8] = try SessionUtil.generateSubaccountToken(
groupSessionId: sessionId,
memberId: memberId,
using: dependencies
)
let inviteDetails: GroupInviteMemberJob.Details = try GroupInviteMemberJob.Details(
memberSessionIdHexString: memberId,
authInfo: try SessionUtil.generateAuthData(
@ -531,7 +549,7 @@ extension MessageSender {
/// unrevoke request when initially added them failed (fire-and-forget this request, we don't want it to be blocking)
try SnodeAPI
.preparedUnrevokeSubaccount(
subaccountToUnrevoke: inviteDetails.memberAuthData.toHexString(),
subaccountToUnrevoke: subaccountToken,
authMethod: Authentication.groupAdmin(
groupSessionId: sessionId,
ed25519SecretKey: Array(groupIdentityPrivateKey)
@ -658,16 +676,15 @@ extension MessageSender {
requests: memberIdsChunk.compactMap { id -> HTTP.PreparedRequest<Void>? in
// Generate authData for the removed member
guard
let memberAuthInfo: Authentication.Info = try? SessionUtil.generateAuthData(
let subaccountToken: [UInt8] = try? SessionUtil.generateSubaccountToken(
groupSessionId: groupSessionId,
memberId: id,
using: dependencies
),
case .groupMember(_, let memberAuthData) = memberAuthInfo
)
else { return nil }
return try? SnodeAPI.preparedRevokeSubaccount(
subaccountToRevoke: memberAuthData.toHexString(),
subaccountToRevoke: subaccountToken,
authMethod: Authentication.groupAdmin(
groupSessionId: groupSessionId,
ed25519SecretKey: Array(groupIdentityPrivateKey)

@ -91,6 +91,22 @@ internal extension SessionUtil {
} ?? { throw SessionUtilError.invalidConfigObject }()
}
static func generateSubaccountToken(
groupSessionId: SessionId,
memberId: String,
using dependencies: Dependencies
) throws -> [UInt8] {
try dependencies[singleton: .crypto].perform(
.subaccountToken(
config: dependencies[cache: .sessionUtil]
.config(for: .groupKeys, sessionId: groupSessionId)
.wrappedValue,
groupSessionId: groupSessionId,
memberId: memberId
)
)
}
static func generateAuthData(
groupSessionId: SessionId,
memberId: String,

@ -1238,7 +1238,7 @@ extension SessionUtil {
) \(lastKeyPair, asSubquery: true) ON \(lastKeyPair[.threadId]) = \(closedGroup[.threadId])
LEFT JOIN \(disappearingConfig) ON \(disappearingConfig[.threadId]) = \(closedGroup[.threadId])
WHERE \(SQL("\(closedGroup[.threadId]) LIKE '\(SessionId.Prefix.standard)%'"))
WHERE \(closedGroup[.threadId]) LIKE '\(SessionId.Prefix.standard)%'
"""
let legacyGroupInfoNoMembers: [LegacyGroupInfo] = try request

@ -4,6 +4,32 @@ import Foundation
import SessionUtil
import SessionUtilitiesKit
public extension Crypto.Action {
static func subaccountToken(
config: SessionUtil.Config?,
groupSessionId: SessionId,
memberId: String
) -> Crypto.Action {
return Crypto.Action(
id: "subaccountToken",
args: [config, groupSessionId, memberId]
) {
guard case .groupKeys(let conf, _, _) = config else { throw SessionUtilError.invalidConfigObject }
var cMemberId: [CChar] = memberId.cArray
var tokenData: [UInt8] = [UInt8](repeating: 0, count: SessionUtil.sizeSubaccountBytes)
guard groups_keys_swarm_subaccount_token(
conf,
&cMemberId,
&tokenData
) else { throw SessionUtilError.failedToMakeSubAccountInGroup }
return tokenData
}
}
}
public extension Crypto.AuthenticationInfo {
static func memberAuthData(
config: SessionUtil.Config?,

@ -244,14 +244,14 @@ public struct DisplayPictureManager {
public static func prepareAndUploadDisplayPicture(
queue: DispatchQueue,
imageData: Data,
success: @escaping ((downloadUrl: String, fileName: String, profileKey: Data)) -> (),
success: @escaping ((downloadUrl: String, fileName: String, encryptionKey: Data)) -> (),
failure: ((DisplayPictureError) -> ())? = nil,
using dependencies: Dependencies
) {
queue.async {
// If the profile avatar was updated or removed then encrypt with a new profile key
// to ensure that other users know that our profile picture was updated
let newProfileKey: Data
let newEncryptionKey: Data
let finalImageData: Data
let fileExtension: String
@ -306,7 +306,9 @@ public struct DisplayPictureManager {
return data
}()
newProfileKey = try Randomness.generateRandomBytes(numberBytes: DisplayPictureManager.aes256KeyByteLength)
newEncryptionKey = try Randomness.generateRandomBytes(
numberBytes: DisplayPictureManager.aes256KeyByteLength
)
fileExtension = {
switch guessedFormat {
case .gif: return "gif" // stringlint:disable
@ -338,7 +340,7 @@ public struct DisplayPictureManager {
}
// Encrypt the avatar for upload
guard let encryptedData: Data = DisplayPictureManager.encryptData(data: finalImageData, key: newProfileKey) else {
guard let encryptedData: Data = DisplayPictureManager.encryptData(data: finalImageData, key: newEncryptionKey) else {
SNLog("Updating service with profile failed.")
failure?(.encryptionFailed)
return
@ -373,7 +375,7 @@ public struct DisplayPictureManager {
dependencies.mutate(cache: .displayPicture) { $0.imageData[fileName] = finalImageData }
SNLog("Successfully uploaded avatar image.")
success((downloadUrl, fileName, newProfileKey))
success((downloadUrl, fileName, newEncryptionKey))
}
)
}

@ -172,7 +172,7 @@ class MessageSenderGroupsSpec: QuickSpec {
.createGroup(
name: "TestGroupName",
description: nil,
displayPicture: nil,
displayPictureData: nil,
members: [
("051111111111111111111111111111111111111111111111111111111111111111", nil)
],
@ -199,7 +199,7 @@ class MessageSenderGroupsSpec: QuickSpec {
.createGroup(
name: "Test",
description: nil,
displayPicture: nil,
displayPictureData: nil,
members: [
("051111111111111111111111111111111111111111111111111111111111111111", nil)
],
@ -226,7 +226,7 @@ class MessageSenderGroupsSpec: QuickSpec {
.createGroup(
name: "TestGroupName",
description: nil,
displayPicture: nil,
displayPictureData: nil,
members: [
("051111111111111111111111111111111111111111111111111111111111111111", nil)
],
@ -253,7 +253,7 @@ class MessageSenderGroupsSpec: QuickSpec {
.createGroup(
name: "TestGroupName",
description: nil,
displayPicture: nil,
displayPictureData: nil,
members: [
("051111111111111111111111111111111111111111111111111111111111111111", nil)
],
@ -286,7 +286,7 @@ class MessageSenderGroupsSpec: QuickSpec {
.createGroup(
name: "TestGroupName",
description: nil,
displayPicture: nil,
displayPictureData: nil,
members: [
("051111111111111111111111111111111111111111111111111111111111111111", nil)
],
@ -361,7 +361,7 @@ class MessageSenderGroupsSpec: QuickSpec {
.createGroup(
name: "TestGroupName",
description: nil,
displayPicture: nil,
displayPictureData: nil,
members: [
("051111111111111111111111111111111111111111111111111111111111111111", nil)
],
@ -395,7 +395,7 @@ class MessageSenderGroupsSpec: QuickSpec {
.createGroup(
name: "TestGroupName",
description: nil,
displayPicture: nil,
displayPictureData: nil,
members: [
("051111111111111111111111111111111111111111111111111111111111111111", nil)
],
@ -413,7 +413,7 @@ class MessageSenderGroupsSpec: QuickSpec {
.createGroup(
name: "TestGroupName",
description: nil,
displayPicture: nil,
displayPictureData: nil,
members: [
("051111111111111111111111111111111111111111111111111111111111111111", nil)
],
@ -442,7 +442,7 @@ class MessageSenderGroupsSpec: QuickSpec {
.createGroup(
name: "TestGroupName",
description: nil,
displayPicture: nil,
displayPictureData: nil,
members: [
("051111111111111111111111111111111111111111111111111111111111111111", nil)
],

@ -9,24 +9,29 @@ extension SnodeAPI {
case subaccountToRevoke = "revoke"
}
let subaccountToRevoke: String
let subaccountToRevoke: [UInt8]
override var verificationBytes: [UInt8] {
/// Ed25519 signature of `("revoke_subaccount" || subaccount)`; this signs the subkey tag,
/// Ed25519 signature of `("revoke_subaccount" || timestamp || SUBACCOUNT_TAG_BYTES)`; this signs the subkey tag,
/// using `pubkey` to sign. Must be base64 encoded for json requests; binary for OMQ requests.
SnodeAPI.Endpoint.revokeSubaccount.path.bytes
.appending(contentsOf: subaccountToRevoke.bytes)
.appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes)
.appending(contentsOf: subaccountToRevoke)
}
// MARK: - Init
public init(
subaccountToRevoke: String,
authMethod: AuthenticationMethod
subaccountToRevoke: [UInt8],
authMethod: AuthenticationMethod,
timestampMs: UInt64
) {
self.subaccountToRevoke = subaccountToRevoke
super.init(authMethod: authMethod)
super.init(
authMethod: authMethod,
timestampMs: timestampMs
)
}
// MARK: - Coding
@ -34,7 +39,8 @@ extension SnodeAPI {
override public func encode(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
try container.encode(subaccountToRevoke, forKey: .subaccountToRevoke)
/// The `subaccountToRevoke` should be sent as a hex string
try container.encode(subaccountToRevoke.toHexString(), forKey: .subaccountToRevoke)
try super.encode(to: encoder)
}

@ -9,7 +9,7 @@ public class RevokeSubaccountResponse: SnodeRecursiveResponse<SnodeSwarmItem> {}
// MARK: - ValidatableResponse
extension RevokeSubaccountResponse: ValidatableResponse {
typealias ValidationData = String
typealias ValidationData = (subaccountToRevoke: [UInt8], timestampMs: UInt64)
typealias ValidationResponse = Bool
/// All responses in the swarm must be valid
@ -17,7 +17,7 @@ extension RevokeSubaccountResponse: ValidatableResponse {
internal func validResultMap(
publicKey: String,
validationData: String,
validationData: (subaccountToRevoke: [UInt8], timestampMs: UInt64),
using dependencies: Dependencies
) throws -> [String: Bool] {
let validationMap: [String: Bool] = try swarm.reduce(into: [:]) { result, next in
@ -35,10 +35,11 @@ extension RevokeSubaccountResponse: ValidatableResponse {
return
}
/// Signature of `( PUBKEY_HEX || SUBACCOUNT_TAG_BYTES )` where `SUBACCOUNT_TAG_BYTES` is the
/// Signature of `( PUBKEY_HEX || timestamp || SUBACCOUNT_TAG_BYTES )` where `SUBACCOUNT_TAG_BYTES` is the
/// requested subkey tag for revocation
let verificationBytes: [UInt8] = publicKey.bytes
.appending(contentsOf: validationData.bytes)
.appending(contentsOf: "\(validationData.timestampMs)".data(using: .ascii)?.bytes)
.appending(contentsOf: validationData.subaccountToRevoke)
let isValid: Bool = dependencies[singleton: .crypto].verify(
.signature(

@ -102,7 +102,6 @@ extension SendMessagesResponse: ValidatableResponse {
/// Signature of `hash` signed by the node's ed25519 pubkey
let verificationBytes: [UInt8] = hash.bytes
result[next.key] = dependencies[singleton: .crypto].verify(
.signature(
message: verificationBytes,

@ -9,24 +9,29 @@ extension SnodeAPI {
case subaccountToUnrevoke = "unrevoke"
}
let subaccountToUnrevoke: String
let subaccountToUnrevoke: [UInt8]
override var verificationBytes: [UInt8] {
/// Ed25519 signature of `("unrevoke_subaccount" || subaccount)`; this signs the subkey tag,
/// Ed25519 signature of `("unrevoke_subaccount" || timestamp || subaccount)`; this signs the subkey tag,
/// using `pubkey` to sign. Must be base64 encoded for json requests; binary for OMQ requests.
SnodeAPI.Endpoint.unrevokeSubaccount.path.bytes
.appending(contentsOf: subaccountToUnrevoke.bytes)
.appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes)
.appending(contentsOf: subaccountToUnrevoke)
}
// MARK: - Init
public init(
subaccountToUnrevoke: String,
authMethod: AuthenticationMethod
subaccountToUnrevoke: [UInt8],
authMethod: AuthenticationMethod,
timestampMs: UInt64
) {
self.subaccountToUnrevoke = subaccountToUnrevoke
super.init(authMethod: authMethod)
super.init(
authMethod: authMethod,
timestampMs: timestampMs
)
}
// MARK: - Coding
@ -34,7 +39,8 @@ extension SnodeAPI {
override public func encode(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
try container.encode(subaccountToUnrevoke, forKey: .subaccountToUnrevoke)
/// The `subaccountToRevoke` should be sent as a hex string
try container.encode(subaccountToUnrevoke.toHexString(), forKey: .subaccountToUnrevoke)
try super.encode(to: encoder)
}

@ -9,7 +9,7 @@ public class UnrevokeSubaccountResponse: SnodeRecursiveResponse<SnodeSwarmItem>
// MARK: - ValidatableResponse
extension UnrevokeSubaccountResponse: ValidatableResponse {
typealias ValidationData = String
typealias ValidationData = (subaccountToUnrevoke: [UInt8], timestampMs: UInt64)
typealias ValidationResponse = Bool
/// All responses in the swarm must be valid
@ -17,7 +17,7 @@ extension UnrevokeSubaccountResponse: ValidatableResponse {
internal func validResultMap(
publicKey: String,
validationData: String,
validationData: (subaccountToUnrevoke: [UInt8], timestampMs: UInt64),
using dependencies: Dependencies
) throws -> [String: Bool] {
let validationMap: [String: Bool] = try swarm.reduce(into: [:]) { result, next in
@ -35,10 +35,11 @@ extension UnrevokeSubaccountResponse: ValidatableResponse {
return
}
/// Signature of `( PUBKEY_HEX || SUBKEY_TAG_BYTES )` where `SUBKEY_TAG_BYTES` is the
/// Signature of `( PUBKEY_HEX || timestamp || SUBKEY_TAG_BYTES )` where `SUBKEY_TAG_BYTES` is the
/// requested subkey tag for revocation
let verificationBytes: [UInt8] = publicKey.bytes
.appending(contentsOf: validationData.bytes)
.appending(contentsOf: "\(validationData.timestampMs)".data(using: .ascii)?.bytes)
.appending(contentsOf: validationData.subaccountToUnrevoke)
let isValid: Bool = dependencies[singleton: .crypto].verify(
.signature(

@ -658,10 +658,12 @@ public final class SnodeAPI {
}
public static func preparedRevokeSubaccount(
subaccountToRevoke: String,
subaccountToRevoke: [UInt8],
authMethod: AuthenticationMethod,
using dependencies: Dependencies = Dependencies()
) throws -> HTTP.PreparedRequest<Void> {
let timestampMs: UInt64 = UInt64(SnodeAPI.currentOffsetTimestampMs(using: dependencies))
return try SnodeAPI
.prepareRequest(
request: Request(
@ -669,7 +671,8 @@ public final class SnodeAPI {
publicKey: authMethod.sessionId.hexString,
body: RevokeSubaccountRequest(
subaccountToRevoke: subaccountToRevoke,
authMethod: authMethod
authMethod: authMethod,
timestampMs: timestampMs
)
),
responseType: RevokeSubaccountResponse.self
@ -677,7 +680,7 @@ public final class SnodeAPI {
.tryMap { _, response -> Void in
try response.validateResultMap(
publicKey: authMethod.sessionId.hexString,
validationData: subaccountToRevoke,
validationData: (subaccountToRevoke, timestampMs),
using: dependencies
)
@ -686,10 +689,12 @@ public final class SnodeAPI {
}
public static func preparedUnrevokeSubaccount(
subaccountToUnrevoke: String,
subaccountToUnrevoke: [UInt8],
authMethod: AuthenticationMethod,
using dependencies: Dependencies = Dependencies()
) throws -> HTTP.PreparedRequest<Void> {
let timestampMs: UInt64 = UInt64(SnodeAPI.currentOffsetTimestampMs(using: dependencies))
return try SnodeAPI
.prepareRequest(
request: Request(
@ -697,7 +702,8 @@ public final class SnodeAPI {
publicKey: authMethod.sessionId.hexString,
body: UnrevokeSubaccountRequest(
subaccountToUnrevoke: subaccountToUnrevoke,
authMethod: authMethod
authMethod: authMethod,
timestampMs: timestampMs
)
),
responseType: UnrevokeSubaccountResponse.self
@ -705,7 +711,7 @@ public final class SnodeAPI {
.tryMap { _, response -> Void in
try response.validateResultMap(
publicKey: authMethod.sessionId.hexString,
validationData: subaccountToUnrevoke,
validationData: (subaccountToUnrevoke, timestampMs),
using: dependencies
)

@ -77,6 +77,6 @@ public extension SQLInterpolation {
/// let request: SQLRequest<User> = "SELECT * FROM \(user) WHERE \(user[.id]) LIKE '\(SessionId.Prefix.standard)%'"
@_disfavoredOverload
mutating func appendInterpolation(_ idPrefix: SessionId.Prefix) {
appendLiteral("\(SQL(stringLiteral: "\(idPrefix.rawValue)"))")
appendLiteral(idPrefix.rawValue)
}
}

@ -56,6 +56,7 @@ public class Dependencies {
/// in `Dependencies.cacheInstances` so that we can be reliably certail we aren't accessing some
/// random instance that will go out of memory as soon as the mutation is completed
getValueSettingIfNull(cache: cache, &Dependencies.cacheInstances)
let cacheWrapper: Atomic<MutableCacheType> = (
Dependencies.cacheInstances.wrappedValue[cache.identifier] ??
Atomic(cache.mutableInstance(cache.createInstance(self))) // Should never be called

Loading…
Cancel
Save