Implement OpenGroupAPIV2
This commit is contained in:
parent
d11db4cb03
commit
34bbff1ab4
|
@ -251,6 +251,7 @@
|
||||||
B886B4A72398B23E00211ABE /* QRCodeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B886B4A62398B23E00211ABE /* QRCodeVC.swift */; };
|
B886B4A72398B23E00211ABE /* QRCodeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B886B4A62398B23E00211ABE /* QRCodeVC.swift */; };
|
||||||
B886B4A92398BA1500211ABE /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B886B4A82398BA1500211ABE /* QRCode.swift */; };
|
B886B4A92398BA1500211ABE /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B886B4A82398BA1500211ABE /* QRCode.swift */; };
|
||||||
B88A1AC725C90A4700E6D421 /* TypingIndicatorInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B6A904218B4C90007C4606 /* TypingIndicatorInteraction.swift */; };
|
B88A1AC725C90A4700E6D421 /* TypingIndicatorInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B6A904218B4C90007C4606 /* TypingIndicatorInteraction.swift */; };
|
||||||
|
B88FA7B826045D100049422F /* OpenGroupAPIV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88FA7B726045D100049422F /* OpenGroupAPIV2.swift */; };
|
||||||
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; };
|
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; };
|
||||||
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; };
|
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; };
|
||||||
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */; };
|
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */; };
|
||||||
|
@ -310,6 +311,7 @@
|
||||||
C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */; };
|
C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */; };
|
||||||
C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */; };
|
C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */; };
|
||||||
C31FFE57254A5FFE00F19441 /* KeyPairUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */; };
|
C31FFE57254A5FFE00F19441 /* KeyPairUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */; };
|
||||||
|
C3227FF6260AAD66006EA627 /* OpenGroupMessageV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */; };
|
||||||
C32824D325C9F9790062D0A7 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; };
|
C32824D325C9F9790062D0A7 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; };
|
||||||
C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328250E25CA06020062D0A7 /* VoiceMessageView.swift */; };
|
C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328250E25CA06020062D0A7 /* VoiceMessageView.swift */; };
|
||||||
C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328251E25CA3A900062D0A7 /* QuoteView.swift */; };
|
C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328251E25CA3A900062D0A7 /* QuoteView.swift */; };
|
||||||
|
@ -1234,6 +1236,7 @@
|
||||||
B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = "<group>"; };
|
B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = "<group>"; };
|
||||||
B886B4A62398B23E00211ABE /* QRCodeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeVC.swift; sourceTree = "<group>"; };
|
B886B4A62398B23E00211ABE /* QRCodeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeVC.swift; sourceTree = "<group>"; };
|
||||||
B886B4A82398BA1500211ABE /* QRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCode.swift; sourceTree = "<group>"; };
|
B886B4A82398BA1500211ABE /* QRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCode.swift; sourceTree = "<group>"; };
|
||||||
|
B88FA7B726045D100049422F /* OpenGroupAPIV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPIV2.swift; sourceTree = "<group>"; };
|
||||||
B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = "<group>"; };
|
B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = "<group>"; };
|
||||||
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = "<group>"; };
|
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = "<group>"; };
|
||||||
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = "<group>"; };
|
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1306,6 +1309,7 @@
|
||||||
C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionVC.swift; sourceTree = "<group>"; };
|
C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionVC.swift; sourceTree = "<group>"; };
|
||||||
C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUtilities.swift; sourceTree = "<group>"; };
|
C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUtilities.swift; sourceTree = "<group>"; };
|
||||||
C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPairUtilities.swift; sourceTree = "<group>"; };
|
C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPairUtilities.swift; sourceTree = "<group>"; };
|
||||||
|
C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupMessageV2.swift; sourceTree = "<group>"; };
|
||||||
C328250E25CA06020062D0A7 /* VoiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView.swift; sourceTree = "<group>"; };
|
C328250E25CA06020062D0A7 /* VoiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView.swift; sourceTree = "<group>"; };
|
||||||
C328251E25CA3A900062D0A7 /* QuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView.swift; sourceTree = "<group>"; };
|
C328251E25CA3A900062D0A7 /* QuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView.swift; sourceTree = "<group>"; };
|
||||||
C328252F25CA55360062D0A7 /* ContextMenuWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuWindow.swift; sourceTree = "<group>"; };
|
C328252F25CA55360062D0A7 /* ContextMenuWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuWindow.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2495,6 +2499,26 @@
|
||||||
path = Meta;
|
path = Meta;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
C3227FF4260AAD58006EA627 /* V2 */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
B88FA7B726045D100049422F /* OpenGroupAPIV2.swift */,
|
||||||
|
C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */,
|
||||||
|
);
|
||||||
|
path = V2;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
C3228005260AAD7E006EA627 /* V1 */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C3A721352558BDF90043A11F /* OpenGroupAPI.swift */,
|
||||||
|
C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */,
|
||||||
|
C3A721342558BDF90043A11F /* OpenGroupMessage.swift */,
|
||||||
|
C3A7229B2558E4310043A11F /* OpenGroupMessage+Conversion.swift */,
|
||||||
|
);
|
||||||
|
path = V1;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
C328252E25CA54F70062D0A7 /* Context Menu */ = {
|
C328252E25CA54F70062D0A7 /* Context Menu */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -3158,11 +3182,9 @@
|
||||||
C3A721332558BDDF0043A11F /* Open Groups */ = {
|
C3A721332558BDDF0043A11F /* Open Groups */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
C3228005260AAD7E006EA627 /* V1 */,
|
||||||
|
C3227FF4260AAD58006EA627 /* V2 */,
|
||||||
C3A721372558BDFA0043A11F /* OpenGroup.swift */,
|
C3A721372558BDFA0043A11F /* OpenGroup.swift */,
|
||||||
C3A721352558BDF90043A11F /* OpenGroupAPI.swift */,
|
|
||||||
C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */,
|
|
||||||
C3A721342558BDF90043A11F /* OpenGroupMessage.swift */,
|
|
||||||
C3A7229B2558E4310043A11F /* OpenGroupMessage+Conversion.swift */,
|
|
||||||
C3AAFFCB25AE92150089E6DD /* OpenGroupManager.swift */,
|
C3AAFFCB25AE92150089E6DD /* OpenGroupManager.swift */,
|
||||||
);
|
);
|
||||||
path = "Open Groups";
|
path = "Open Groups";
|
||||||
|
@ -4733,6 +4755,7 @@
|
||||||
C32C5BDD256DC88D003C73A2 /* OWSReadReceiptManager.m in Sources */,
|
C32C5BDD256DC88D003C73A2 /* OWSReadReceiptManager.m in Sources */,
|
||||||
C3D9E3BF25676AD70040E4F3 /* TSAttachmentStream.m in Sources */,
|
C3D9E3BF25676AD70040E4F3 /* TSAttachmentStream.m in Sources */,
|
||||||
C3C2A7562553A3AB00C340D1 /* VisibleMessage+Quote.swift in Sources */,
|
C3C2A7562553A3AB00C340D1 /* VisibleMessage+Quote.swift in Sources */,
|
||||||
|
C3227FF6260AAD66006EA627 /* OpenGroupMessageV2.swift in Sources */,
|
||||||
B8B32021258B1A650020074B /* Contact.swift in Sources */,
|
B8B32021258B1A650020074B /* Contact.swift in Sources */,
|
||||||
C32C5C89256DD0D2003C73A2 /* Storage+Jobs.swift in Sources */,
|
C32C5C89256DD0D2003C73A2 /* Storage+Jobs.swift in Sources */,
|
||||||
C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */,
|
C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */,
|
||||||
|
@ -4801,6 +4824,7 @@
|
||||||
C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */,
|
C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */,
|
||||||
C3A7222A2558C1E40043A11F /* DotNetAPI.swift in Sources */,
|
C3A7222A2558C1E40043A11F /* DotNetAPI.swift in Sources */,
|
||||||
C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */,
|
C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */,
|
||||||
|
B88FA7B826045D100049422F /* OpenGroupAPIV2.swift in Sources */,
|
||||||
C32C5E97256DE0CB003C73A2 /* OWSPrimaryStorage.m in Sources */,
|
C32C5E97256DE0CB003C73A2 /* OWSPrimaryStorage.m in Sources */,
|
||||||
C32C5EB9256DE130003C73A2 /* OWSQuotedReplyModel+Conversion.swift in Sources */,
|
C32C5EB9256DE130003C73A2 /* OWSQuotedReplyModel+Conversion.swift in Sources */,
|
||||||
C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */,
|
C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */,
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
import PromiseKit
|
||||||
|
import SessionSnodeKit
|
||||||
|
|
||||||
|
// TODO: Auth token & public key storage
|
||||||
|
|
||||||
|
public enum OpenGroupAPIV2 {
|
||||||
|
|
||||||
|
// MARK: Error
|
||||||
|
public enum Error : LocalizedError {
|
||||||
|
case generic
|
||||||
|
case parsingFailed
|
||||||
|
case decryptionFailed
|
||||||
|
case signingFailed
|
||||||
|
case invalidURL
|
||||||
|
case noPublicKey
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .generic: return "An error occurred."
|
||||||
|
case .parsingFailed: return "Invalid response."
|
||||||
|
case .decryptionFailed: return "Couldn't decrypt response."
|
||||||
|
case .signingFailed: return "Couldn't sign message."
|
||||||
|
case .invalidURL: return "Invalid URL."
|
||||||
|
case .noPublicKey: return "Couldn't find server public key."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Request
|
||||||
|
private struct Request {
|
||||||
|
let verb: HTTP.Verb
|
||||||
|
let room: String
|
||||||
|
let server: String
|
||||||
|
let endpoint: String
|
||||||
|
let queryParameters: [String:String]
|
||||||
|
let parameters: JSON
|
||||||
|
let isAuthRequired: Bool
|
||||||
|
/// Always `true` under normal circumstances. You might want to disable
|
||||||
|
/// this when running over Lokinet.
|
||||||
|
let useOnionRouting: Bool
|
||||||
|
|
||||||
|
init(verb: HTTP.Verb, room: String, server: String, endpoint: String, queryParameters: [String:String] = [:],
|
||||||
|
parameters: JSON = [:], isAuthRequired: Bool = true, useOnionRouting: Bool = true) {
|
||||||
|
self.verb = verb
|
||||||
|
self.room = room
|
||||||
|
self.server = server
|
||||||
|
self.endpoint = endpoint
|
||||||
|
self.queryParameters = queryParameters
|
||||||
|
self.parameters = parameters
|
||||||
|
self.isAuthRequired = isAuthRequired
|
||||||
|
self.useOnionRouting = useOnionRouting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Convenience
|
||||||
|
private static func send(_ request: Request) -> Promise<JSON> {
|
||||||
|
let tsRequest: TSRequest
|
||||||
|
switch request.verb {
|
||||||
|
case .get:
|
||||||
|
var rawURL = "\(request.server)/\(request.endpoint)"
|
||||||
|
if !request.queryParameters.isEmpty {
|
||||||
|
let queryString = request.queryParameters.map { key, value in "\(key)=\(value)" }.joined(separator: "&")
|
||||||
|
rawURL += "?\(queryString)"
|
||||||
|
}
|
||||||
|
guard let url = URL(string: rawURL) else { return Promise(error: Error.invalidURL) }
|
||||||
|
tsRequest = TSRequest(url: url)
|
||||||
|
case .post, .put, .delete:
|
||||||
|
let rawURL = "\(request.server)/\(request.endpoint)"
|
||||||
|
guard let url = URL(string: rawURL) else { return Promise(error: Error.invalidURL) }
|
||||||
|
tsRequest = TSRequest(url: url, method: request.verb.rawValue, parameters: request.parameters)
|
||||||
|
}
|
||||||
|
tsRequest.setValue(request.room, forKey: "Room")
|
||||||
|
if request.useOnionRouting {
|
||||||
|
guard let publicKey = SNMessagingKitConfiguration.shared.storage.getOpenGroupPublicKey(for: request.server) else { return Promise(error: Error.noPublicKey) }
|
||||||
|
return getAuthToken(for: request.server).then(on: DispatchQueue.global(qos: .default)) { authToken -> Promise<JSON> in
|
||||||
|
tsRequest.setValue(authToken, forKey: "Authorization")
|
||||||
|
return OnionRequestAPI.sendOnionRequest(tsRequest, to: request.server, using: publicKey)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
preconditionFailure("It's currently not allowed to send non onion routed requests.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Authorization
|
||||||
|
private static func getAuthToken(for server: String) -> Promise<String> {
|
||||||
|
return Promise.value("") // TODO: Implement
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func requestNewAuthToken(for room: String, on server: String) -> Promise<String> {
|
||||||
|
SNLog("Requesting auth token for server: \(server).")
|
||||||
|
guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { return Promise(error: Error.generic) }
|
||||||
|
let queryParameters = [ "public_key" : getUserHexEncodedPublicKey() ]
|
||||||
|
let request = Request(verb: .get, room: room, server: server, endpoint: "auth_token_challenge", queryParameters: queryParameters, isAuthRequired: false)
|
||||||
|
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||||
|
guard let base64EncodedCiphertext = json["ciphertext"] as? String, let base64EncodedEphemeralPublicKey = json["ephemeral_public_key"] as? String,
|
||||||
|
let ciphertext = Data(base64Encoded: base64EncodedCiphertext), let ephemeralPublicKey = Data(base64Encoded: base64EncodedEphemeralPublicKey) else {
|
||||||
|
throw Error.parsingFailed
|
||||||
|
}
|
||||||
|
let symmetricKey = try AESGCM.generateSymmetricKey(x25519PublicKey: ephemeralPublicKey, x25519PrivateKey: userKeyPair.privateKey)
|
||||||
|
guard let tokenAsData = try? AESGCM.decrypt(ciphertext, with: symmetricKey) else { throw Error.decryptionFailed }
|
||||||
|
return tokenAsData.toHexString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func claimAuthToken(for room: String, on server: String) -> Promise<Void> {
|
||||||
|
guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { return Promise(error: Error.generic) }
|
||||||
|
let parameters = [ "public_key" : userKeyPair.publicKey.toHexString() ]
|
||||||
|
let request = Request(verb: .post, room: room, server: server, endpoint: "claim_auth_token", parameters: parameters)
|
||||||
|
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { _ in }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should be called when leaving a group.
|
||||||
|
public static func deleteAuthToken(for room: String, on server: String) -> Promise<Void> {
|
||||||
|
let request = Request(verb: .delete, room: room, server: server, endpoint: "auth_token")
|
||||||
|
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { _ in }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: File Storage
|
||||||
|
public static func upload(_ file: Data, to room: String, on server: String) -> Promise<String> {
|
||||||
|
let base64EncodedFile = file.base64EncodedString()
|
||||||
|
let parameters = [ "file" : base64EncodedFile ]
|
||||||
|
let request = Request(verb: .post, room: room, server: server, endpoint: "files", parameters: parameters)
|
||||||
|
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||||
|
guard let fileID = json["result"] as? String else { throw Error.parsingFailed }
|
||||||
|
return fileID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func download(_ file: String, from room: String, on server: String) -> Promise<Data> {
|
||||||
|
let request = Request(verb: .get, room: room, server: server, endpoint: "files/\(file)")
|
||||||
|
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||||
|
guard let base64EncodedFile = json["result"] as? String, let file = Data(base64Encoded: base64EncodedFile) else { throw Error.parsingFailed }
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Message Sending & Receiving
|
||||||
|
public static func send(_ message: OpenGroupMessageV2, to room: String, on server: String) -> Promise<OpenGroupMessageV2> {
|
||||||
|
guard let signedMessage = message.sign() else { return Promise(error: Error.signingFailed) }
|
||||||
|
guard let json = signedMessage.toJSON() else { return Promise(error: Error.parsingFailed) }
|
||||||
|
let request = Request(verb: .post, room: room, server: server, endpoint: "messages", parameters: json)
|
||||||
|
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||||
|
guard let message = OpenGroupMessageV2.fromJSON(json) else { throw Error.parsingFailed }
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func getMessages(for room: String, on server: String) -> Promise<[OpenGroupMessageV2]> {
|
||||||
|
// TODO: From server ID & limit
|
||||||
|
let queryParameters: [String:String] = [:]
|
||||||
|
let request = Request(verb: .get, room: room, server: server, endpoint: "messages", queryParameters: queryParameters)
|
||||||
|
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||||
|
guard let rawMessages = json["messages"] as? [[String:Any]] else { throw Error.parsingFailed }
|
||||||
|
let messages: [OpenGroupMessageV2] = rawMessages.compactMap { json in
|
||||||
|
// TODO: Signature validation
|
||||||
|
guard let message = OpenGroupMessageV2.fromJSON(json) else {
|
||||||
|
SNLog("Couldn't parse open group message from JSON: \(json).")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Message Deletion
|
||||||
|
public static func deleteMessage(with serverID: Int64, from room: String, on server: String) -> Promise<Void> {
|
||||||
|
let request = Request(verb: .delete, room: room, server: server, endpoint: "messages/\(serverID)")
|
||||||
|
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { _ in }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func getDeletedMessages(for room: String, on server: String) -> Promise<[Int64]> {
|
||||||
|
// TODO: From server ID & limit
|
||||||
|
let queryParameters: [String:String] = [:]
|
||||||
|
let request = Request(verb: .get, room: room, server: server, endpoint: "deleted_messages", queryParameters: queryParameters)
|
||||||
|
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||||
|
guard let ids = json["ids"] as? [Int64] else { throw Error.parsingFailed }
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Moderation
|
||||||
|
public static func getModerators(for room: String, on server: String) -> Promise<[String]> {
|
||||||
|
let request = Request(verb: .get, room: room, server: server, endpoint: "moderators")
|
||||||
|
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||||
|
guard let moderators = json["moderators"] as? [String] else { throw Error.parsingFailed }
|
||||||
|
return moderators
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func ban(_ publicKey: String, from room: String, on server: String) -> Promise<Void> {
|
||||||
|
let parameters = [ "public_key" : publicKey ]
|
||||||
|
let request = Request(verb: .post, room: room, server: server, endpoint: "block_list", parameters: parameters)
|
||||||
|
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { _ in }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func unban(_ publicKey: String, from room: String, on server: String) -> Promise<Void> {
|
||||||
|
let request = Request(verb: .delete, room: room, server: server, endpoint: "block_list/\(publicKey)")
|
||||||
|
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { _ in }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: General
|
||||||
|
public static func getMemberCount(for room: String, on server: String) -> Promise<UInt> {
|
||||||
|
let request = Request(verb: .get, room: room, server: server, endpoint: "member_count")
|
||||||
|
return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||||
|
guard let memberCount = json["member_count"] as? UInt else { throw Error.parsingFailed }
|
||||||
|
return memberCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
|
||||||
|
public struct OpenGroupMessageV2 {
|
||||||
|
public let serverID: Int64?
|
||||||
|
/// The serialized protobuf in base64 encoding.
|
||||||
|
public let base64EncodedData: String
|
||||||
|
/// When sending a message, the sender signs the serialized protobuf with their private key so that
|
||||||
|
/// a receiving user can verify that the message wasn't tampered with.
|
||||||
|
public let base64EncodedSignature: String?
|
||||||
|
|
||||||
|
public func sign() -> OpenGroupMessageV2? {
|
||||||
|
let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair()!
|
||||||
|
let data = Data(base64Encoded: base64EncodedData)!
|
||||||
|
guard let signature = try? Ed25519.sign(data, with: userKeyPair) else {
|
||||||
|
SNLog("Failed to sign open group message.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return OpenGroupMessageV2(serverID: serverID, base64EncodedData: base64EncodedData, base64EncodedSignature: signature.base64EncodedString())
|
||||||
|
}
|
||||||
|
|
||||||
|
public func toJSON() -> JSON? {
|
||||||
|
var result: JSON = [ "data" : base64EncodedData ]
|
||||||
|
if let serverID = serverID { result["server_id"] = serverID }
|
||||||
|
if let base64EncodedSignature = base64EncodedSignature { result["signature"] = base64EncodedSignature }
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func fromJSON(_ json: JSON) -> OpenGroupMessageV2? {
|
||||||
|
guard let base64EncodedData = json["data"] as? String else { return nil }
|
||||||
|
let serverID = json["server_id"] as? Int64
|
||||||
|
let base64EncodedSignature = json["signature"] as? String
|
||||||
|
return OpenGroupMessageV2(serverID: serverID, base64EncodedData: base64EncodedData, base64EncodedSignature: base64EncodedSignature)
|
||||||
|
}
|
||||||
|
}
|
|
@ -374,12 +374,16 @@ public enum OnionRequestAPI {
|
||||||
let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= AESGCM.ivSize else { return seal.reject(HTTP.Error.invalidJSON) }
|
let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= AESGCM.ivSize else { return seal.reject(HTTP.Error.invalidJSON) }
|
||||||
do {
|
do {
|
||||||
let data = try AESGCM.decrypt(ivAndCiphertext, with: destinationSymmetricKey)
|
let data = try AESGCM.decrypt(ivAndCiphertext, with: destinationSymmetricKey)
|
||||||
|
// The old open group server and file server implementations put the status code in the JSON under the "status"
|
||||||
|
// key, whereas the new implementations put it under the "status_code" key
|
||||||
guard let json = try JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON,
|
guard let json = try JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON,
|
||||||
let statusCode = json["status"] as? Int else { return seal.reject(HTTP.Error.invalidJSON) }
|
let statusCode = json["status_code"] as? Int ?? json["status"] as? Int else { return seal.reject(HTTP.Error.invalidJSON) }
|
||||||
if statusCode == 406 { // Clock out of sync
|
if statusCode == 406 { // Clock out of sync
|
||||||
SNLog("The user's clock is out of sync with the service node network.")
|
SNLog("The user's clock is out of sync with the service node network.")
|
||||||
seal.reject(SnodeAPI.Error.clockOutOfSync)
|
seal.reject(SnodeAPI.Error.clockOutOfSync)
|
||||||
} else if let bodyAsString = json["body"] as? String {
|
} else if let bodyAsString = json["body"] as? String {
|
||||||
|
// This clause is only used by the old open group and file server implementations. The new implementations will
|
||||||
|
// always go to the next clause.
|
||||||
let body: JSON
|
let body: JSON
|
||||||
if !isJSONRequired {
|
if !isJSONRequired {
|
||||||
body = [ "result" : bodyAsString ]
|
body = [ "result" : bodyAsString ]
|
||||||
|
|
|
@ -19,6 +19,20 @@ public enum AESGCM {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// - Note: Sync. Don't call from the main thread.
|
||||||
|
public static func generateSymmetricKey(x25519PublicKey: Data, x25519PrivateKey: Data) throws -> Data {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
#if DEBUG
|
||||||
|
preconditionFailure("It's illegal to call encrypt(_:forSnode:) from the main thread.")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
guard let sharedSecret = try? Curve25519.generateSharedSecret(fromPublicKey: x25519PublicKey, privateKey: x25519PrivateKey) else {
|
||||||
|
throw Error.sharedSecretGenerationFailed
|
||||||
|
}
|
||||||
|
let salt = "LOKI"
|
||||||
|
return try Data(HMAC(key: salt.bytes, variant: .sha256).authenticate(sharedSecret.bytes))
|
||||||
|
}
|
||||||
|
|
||||||
/// - Note: Sync. Don't call from the main thread.
|
/// - Note: Sync. Don't call from the main thread.
|
||||||
public static func decrypt(_ ivAndCiphertext: Data, with symmetricKey: Data) throws -> Data {
|
public static func decrypt(_ ivAndCiphertext: Data, with symmetricKey: Data) throws -> Data {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
|
@ -56,11 +70,7 @@ public enum AESGCM {
|
||||||
}
|
}
|
||||||
let x25519PublicKey = Data(hex: hexEncodedX25519PublicKey)
|
let x25519PublicKey = Data(hex: hexEncodedX25519PublicKey)
|
||||||
let ephemeralKeyPair = Curve25519.generateKeyPair()
|
let ephemeralKeyPair = Curve25519.generateKeyPair()
|
||||||
guard let ephemeralSharedSecret = try? Curve25519.generateSharedSecret(fromPublicKey: x25519PublicKey, privateKey: ephemeralKeyPair.privateKey) else {
|
let symmetricKey = try generateSymmetricKey(x25519PublicKey: x25519PublicKey, x25519PrivateKey: ephemeralKeyPair.privateKey)
|
||||||
throw Error.sharedSecretGenerationFailed
|
|
||||||
}
|
|
||||||
let salt = "LOKI"
|
|
||||||
let symmetricKey = try HMAC(key: salt.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes)
|
|
||||||
let ciphertext = try encrypt(plaintext, with: Data(symmetricKey))
|
let ciphertext = try encrypt(plaintext, with: Data(symmetricKey))
|
||||||
return EncryptionResult(ciphertext: ciphertext, symmetricKey: Data(symmetricKey), ephemeralPublicKey: ephemeralKeyPair.publicKey)
|
return EncryptionResult(ciphertext: ciphertext, symmetricKey: Data(symmetricKey), ephemeralPublicKey: ephemeralKeyPair.publicKey)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue