Updated to the latest blinding behaviour

Added a couple more dependencies for unit testing injection
Updated the MessageSender to set the sender of the message to the appropriate blinded/unblinded key
Updated the OpenGroup Message to handle verification of both blinded and unblinded messages
Updated the MessageSender to use dependency injection for it's sendToOpenGroupDestination method
Updated the JSONDecoder to support getting dependencies (for signature verification)
Fixed tests broken by updating the signing logic
This commit is contained in:
Morgan Pretty 2022-02-21 10:01:53 +11:00
parent ef09d4d5aa
commit 1edd500dab
20 changed files with 496 additions and 279 deletions

View File

@ -776,7 +776,6 @@
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; };
FD5D201E27B0D87C00FEA984 /* IdPrefix.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201D27B0D87C00FEA984 /* IdPrefix.swift */; };
FD5D202027B0E67900FEA984 /* String+Encoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201F27B0E67800FEA984 /* String+Encoding.swift */; };
FD5D202227B1D74F00FEA984 /* ECKeyPair+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D202127B1D74F00FEA984 /* ECKeyPair+Conversion.swift */; };
FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */; };
FD705A8C278CDB5600F16121 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */; };
FD705A8E278CE29800F16121 /* String+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8D278CE29800F16121 /* String+Localization.swift */; };
@ -784,6 +783,12 @@
FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; };
FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */; };
FD705A98278E9F4D00F16121 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */; };
FD859EF227BF6BA200510D0C /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; };
FD859EF427C2F49200510D0C /* TestSodium.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF327C2F49200510D0C /* TestSodium.swift */; };
FD859EF627C2F52C00510D0C /* TestSign.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF527C2F52C00510D0C /* TestSign.swift */; };
FD859EF827C2F58900510D0C /* TestAeadXChaCha20Poly1305Ietf.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF727C2F58900510D0C /* TestAeadXChaCha20Poly1305Ietf.swift */; };
FD859EFA27C2F5C500510D0C /* TestGenericHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF927C2F5C500510D0C /* TestGenericHash.swift */; };
FD859EFC27C2F60700510D0C /* TestEd25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFB27C2F60700510D0C /* TestEd25519.swift */; };
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; };
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; };
FDC4380927B31D4E00C60D73 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* Error.swift */; };
@ -1912,7 +1917,6 @@
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
FD5D201D27B0D87C00FEA984 /* IdPrefix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdPrefix.swift; sourceTree = "<group>"; };
FD5D201F27B0E67800FEA984 /* String+Encoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Encoding.swift"; sourceTree = "<group>"; };
FD5D202127B1D74F00FEA984 /* ECKeyPair+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ECKeyPair+Conversion.swift"; sourceTree = "<group>"; };
FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = "<group>"; };
FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = "<group>"; };
FD705A8D278CE29800F16121 /* String+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = "<group>"; };
@ -1920,6 +1924,14 @@
FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; };
FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; };
FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
FD859EEF27BF207700510D0C /* SessionProtos.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = SessionProtos.proto; sourceTree = "<group>"; };
FD859EF027BF207C00510D0C /* WebSocketResources.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = WebSocketResources.proto; sourceTree = "<group>"; };
FD859EF127BF6BA200510D0C /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = "<group>"; };
FD859EF327C2F49200510D0C /* TestSodium.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSodium.swift; sourceTree = "<group>"; };
FD859EF527C2F52C00510D0C /* TestSign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSign.swift; sourceTree = "<group>"; };
FD859EF727C2F58900510D0C /* TestAeadXChaCha20Poly1305Ietf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAeadXChaCha20Poly1305Ietf.swift; sourceTree = "<group>"; };
FD859EF927C2F5C500510D0C /* TestGenericHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGenericHash.swift; sourceTree = "<group>"; };
FD859EFB27C2F60700510D0C /* TestEd25519.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestEd25519.swift; sourceTree = "<group>"; };
FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = "<group>"; };
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = "<group>"; };
FD9039443F7CB729CF71350E /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = "<group>"; };
@ -3384,6 +3396,7 @@
C33FDB01255A580700E217F9 /* AppReadiness.h */,
C33FDB75255A581000E217F9 /* AppReadiness.m */,
FDC4383D27B4708600C60D73 /* Atomic.swift */,
FD859EF127BF6BA200510D0C /* Data+Utilities.swift */,
C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */,
C37F53E8255BA9BB002AEA92 /* Environment.h */,
C37F5402255BA9ED002AEA92 /* Environment.m */,
@ -3423,7 +3436,6 @@
C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */,
C3E7134E251C867C009649BB /* Sodium+Utilities.swift */,
FDC4386827B4E6B700C60D73 /* String+Utlities.swift */,
FD5D202127B1D74F00FEA984 /* ECKeyPair+Conversion.swift */,
FDC4387327B5BB9B00C60D73 /* Promise+Utilities.swift */,
C33FDB31255A580A00E217F9 /* SSKEnvironment.h */,
C33FDAF4255A580600E217F9 /* SSKEnvironment.m */,
@ -3539,6 +3551,8 @@
C3C2A7802553AA6300C340D1 /* Protos */ = {
isa = PBXGroup;
children = (
FD859EEF27BF207700510D0C /* SessionProtos.proto */,
FD859EF027BF207C00510D0C /* WebSocketResources.proto */,
C3C2A7812553AA9000C340D1 /* Generated */,
);
path = Protos;
@ -3938,6 +3952,11 @@
children = (
FDC438BC27BB2AB400C60D73 /* Mockable.swift */,
FDC4389C27BA01F000C60D73 /* TestStorage.swift */,
FD859EF327C2F49200510D0C /* TestSodium.swift */,
FD859EF527C2F52C00510D0C /* TestSign.swift */,
FD859EF727C2F58900510D0C /* TestAeadXChaCha20Poly1305Ietf.swift */,
FD859EF927C2F5C500510D0C /* TestGenericHash.swift */,
FD859EFB27C2F60700510D0C /* TestEd25519.swift */,
);
path = _TestUtilities;
sourceTree = "<group>";
@ -5110,6 +5129,7 @@
C32C5A13256DB7A5003C73A2 /* PushNotificationAPI.swift in Sources */,
FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */,
C32A026325A801AA000ED5D4 /* NSData+messagePadding.m in Sources */,
FD859EF227BF6BA200510D0C /* Data+Utilities.swift in Sources */,
FDC4384927B47F4D00C60D73 /* LegacyCompactPollResponse.swift in Sources */,
C352A3932557883D00338F3E /* JobDelegate.swift in Sources */,
C32C5B84256DC54F003C73A2 /* SSKEnvironment.m in Sources */,
@ -5168,7 +5188,6 @@
FDC4382827B37FD300C60D73 /* LegacyModeratorsResponse.swift in Sources */,
C32C5B3F256DC1DF003C73A2 /* TSQuotedMessage+Conversion.swift in Sources */,
B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */,
FD5D202227B1D74F00FEA984 /* ECKeyPair+Conversion.swift in Sources */,
FDC4381C27B354AC00C60D73 /* LegacyPublicKeyBody.swift in Sources */,
FDC4382F27B383AF00C60D73 /* UnregisterResponse.swift in Sources */,
FDC4386327B4D94E00C60D73 /* OGMessage.swift in Sources */,
@ -5463,8 +5482,13 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FD859EFA27C2F5C500510D0C /* TestGenericHash.swift in Sources */,
FD859EF827C2F58900510D0C /* TestAeadXChaCha20Poly1305Ietf.swift in Sources */,
FD859EF427C2F49200510D0C /* TestSodium.swift in Sources */,
FD859EFC27C2F60700510D0C /* TestEd25519.swift in Sources */,
FDC4389A27BA002500C60D73 /* OpenGroupAPIV2Tests.swift in Sources */,
FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */,
FD859EF627C2F52C00510D0C /* TestSign.swift in Sources */,
FDC4389D27BA01F000C60D73 /* TestStorage.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -62,13 +62,13 @@ extension OpenGroupAPI {
// MARK: - Convenience
public extension Decodable {
static func decoded(from data: Data) throws -> Self {
return try JSONDecoder().decode(Self.self, from: data)
static func decoded(from data: Data, customError: Error, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) throws -> Self {
return try data.decoded(as: Self.self, customError: customError, using: dependencies)
}
}
extension Promise where T == (OnionRequestResponseInfoType, Data?) {
func decoded(as types: OpenGroupAPI.BatchResponseTypes, on queue: DispatchQueue? = nil, error: Error) -> Promise<OpenGroupAPI.BatchResponse> {
func decoded(as types: OpenGroupAPI.BatchResponseTypes, on queue: DispatchQueue? = nil, error: Error, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise<OpenGroupAPI.BatchResponse> {
self.map(on: queue) { responseInfo, maybeData -> OpenGroupAPI.BatchResponse in
// Need to split the data into an array of data so each item can be Decoded correctly
guard let data: Data = maybeData else { throw OpenGroupAPI.Error.parsingFailed }
@ -82,7 +82,7 @@ extension Promise where T == (OnionRequestResponseInfoType, Data?) {
do {
return try zip(dataArray, types)
.map { data, type in try type.decoded(from: data) }
.map { data, type in try type.decoded(from: data, customError: error, using: dependencies) }
.map { data in (responseInfo, data) }
}
catch _ {

View File

@ -47,14 +47,30 @@ extension OpenGroupAPI.Message {
guard let sender: String = maybeSender, let data = Data(base64Encoded: base64EncodedData), let signature = Data(base64Encoded: base64EncodedSignature) else {
throw OpenGroupAPI.Error.parsingFailed
}
let publicKey: Data = Data(hex: sender.removingIdPrefixIfNeeded())
let isValid: Bool = ((try? Ed25519.verifySignature(signature, publicKey: publicKey, data: data)) ?? false)
guard isValid else {
SNLog("Ignoring message with invalid signature.")
guard let dependencies: OpenGroupAPI.Dependencies = decoder.userInfo[OpenGroupAPI.Dependencies.userInfoKey] as? OpenGroupAPI.Dependencies else {
throw OpenGroupAPI.Error.parsingFailed
}
// Verify the signature based on the IdPrefix
let publicKey: Data = Data(hex: sender.removingIdPrefixIfNeeded())
switch IdPrefix(with: sender) {
case .blinded:
guard dependencies.sign.verify(message: data.bytes, publicKey: publicKey.bytes, signature: signature.bytes) else {
SNLog("Ignoring message with invalid signature.")
throw OpenGroupAPI.Error.parsingFailed
}
case .standard, .unblinded:
guard (try? dependencies.ed25519.verifySignature(signature, publicKey: publicKey, data: data)) == true else {
SNLog("Ignoring message with invalid signature.")
throw OpenGroupAPI.Error.parsingFailed
}
case .none:
SNLog("Ignoring message with invalid sender.")
throw OpenGroupAPI.Error.parsingFailed
}
}
self = OpenGroupAPI.Message(

View File

@ -125,7 +125,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: responseTypes, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: responseTypes, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.map { result in
result.enumerated()
.reduce(into: [:]) { prev, next in
@ -156,7 +156,7 @@ public final class OpenGroupAPI: NSObject {
// TODO: Handle a `412` response (ie. a required capability isn't supported)
return send(request, using: dependencies)
.decoded(as: responseTypes, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: responseTypes, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
.map { result in
result.enumerated()
.reduce(into: [:]) { prev, next in
@ -176,7 +176,7 @@ public final class OpenGroupAPI: NSObject {
// TODO: Handle a `412` response (ie. a required capability isn't supported)
return send(request, using: dependencies)
.decoded(as: Capabilities.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: Capabilities.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
// MARK: - Room
@ -188,7 +188,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: [Room].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: [Room].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
public static func room(for roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Room)> {
@ -198,7 +198,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: Room.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: Room.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
public static func roomPollInfo(lastUpdated: Int64, for roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, RoomPollInfo)> {
@ -208,7 +208,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: RoomPollInfo.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: RoomPollInfo.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
// MARK: - Messages
@ -221,13 +221,13 @@ public final class OpenGroupAPI: NSObject {
whisperMods: Bool,
using dependencies: Dependencies = Dependencies()
) -> Promise<(OnionRequestResponseInfoType, Message)> {
guard let signedMessage: (data: Data, signature: Data) = sign(message: plaintext, to: roomToken, on: server, using: dependencies) else {
guard let signResult: (publicKey: String, signature: Bytes) = sign(plaintext.bytes, for: server, using: dependencies) else {
return Promise(error: Error.signingFailed)
}
let requestBody: SendMessageRequest = SendMessageRequest(
data: signedMessage.data,
signature: signedMessage.signature,
data: plaintext,
signature: Data(signResult.signature),
whisperTo: whisperTo,
whisperMods: whisperMods,
fileIds: nil // TODO: Add support for 'fileIds'.
@ -245,7 +245,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: Message.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: Message.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
public static func message(_ id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Message)> {
@ -255,7 +255,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: Message.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: Message.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
public static func messageUpdate(
@ -265,13 +265,13 @@ public final class OpenGroupAPI: NSObject {
on server: String,
using dependencies: Dependencies = Dependencies()
) -> Promise<(OnionRequestResponseInfoType, Data?)> {
guard let signedMessage: (data: Data, signature: Data) = sign(message: plaintext, to: roomToken, on: server, using: dependencies) else {
guard let signResult: (publicKey: String, signature: Bytes) = sign(plaintext.bytes, for: server, using: dependencies) else {
return Promise(error: Error.signingFailed)
}
let requestBody: UpdateMessageRequest = UpdateMessageRequest(
data: signedMessage.data,
signature: signedMessage.signature
data: plaintext,
signature: Data(signResult.signature)
)
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
@ -302,7 +302,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
/// This is the direct request to retrieve recent messages from an Open Group so should be retrieved automatically from the `poll()`
@ -319,7 +319,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
/// This is the direct request to retrieve recent messages from an Open Group so should be retrieved automatically from the `poll()`
@ -335,7 +335,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
// MARK: - Pinning
@ -385,7 +385,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
/// Warning: This approach is less efficient as it expects the data to be base64Encoded (with is 33% larger than binary), please use the binary approach
@ -400,7 +400,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
public static func downloadFile(_ fileId: Int64, from roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data)> {
@ -424,7 +424,7 @@ public final class OpenGroupAPI: NSObject {
)
// TODO: This endpoint is getting rewritten to return just data (properties would come through as headers).
return send(request, using: dependencies)
.decoded(as: FileDownloadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: FileDownloadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
// MARK: - Inbox (Message Requests)
@ -436,7 +436,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: [DirectMessage].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: [DirectMessage].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
public static func messageRequestsSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage])> {
@ -446,7 +446,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: [DirectMessage].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: [DirectMessage].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
public static func sendMessageRequest(_ plaintext: Data, to blindedSessionId: String, on server: String, with serverPublicKey: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage])> {
@ -471,7 +471,7 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: [DirectMessage].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: [DirectMessage].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
// MARK: - Users
@ -581,79 +581,45 @@ public final class OpenGroupAPI: NSObject {
)
return send(request, using: dependencies)
.decoded(as: UserDeleteMessagesResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
.decoded(as: UserDeleteMessagesResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies)
}
// MARK: - Authentication
/// Sign a message to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities)
public static func sign(message: Data, to roomToken: String, on serverName: String, using dependencies: Dependencies = Dependencies()) -> (data: Data, signature: Data)? {
public static func sign(_ messageBytes: Bytes, for serverName: String, using dependencies: Dependencies = Dependencies()) -> (publicKey: String, signature: Bytes)? {
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { return nil }
guard let serverPublicKey: String = dependencies.storage.getOpenGroupPublicKey(for: serverName) else {
return nil
}
let server: Server? = dependencies.storage.getOpenGroupServer(name: serverName)
let targetKeyPair: ECKeyPair
// Determine if we want to sign using standard or blinded keys based on the server capabilities (assume
// unblinded if we have none)
// TODO: Remove this (blinding will be required)
// Check if the server supports blinded keys, if so then sign using the blinded key
if server?.capabilities.capabilities.contains(.blinding) == true {
// TODO: Validate this 'openGroupId' is correct for the 'getOpenGroup' call
let openGroupId: String = "\(serverName).\(roomToken)"
// TODO: Validate this is the correct logic (Most likely not)
guard let openGroup: OpenGroup = Storage.shared.getOpenGroup(for: openGroupId) else { return nil }
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { return nil }
guard let blindedKeyPair: Box.KeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: openGroup.publicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else {
guard let blindedKeyPair: Box.KeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else {
return nil
}
targetKeyPair = blindedKeyPair
}
else {
guard let userKeyPair: ECKeyPair = dependencies.storage.getUserKeyPair() else { return nil }
targetKeyPair = userKeyPair
guard let signatureResult: Bytes = dependencies.sodium.sogsSignature(message: messageBytes, secretKey: userEdKeyPair.secretKey, blindedSecretKey: blindedKeyPair.secretKey, blindedPublicKey: blindedKeyPair.publicKey) else {
return nil
}
return (
publicKey: IdPrefix.blinded.hexEncodedPublicKey(for: blindedKeyPair.publicKey),
signature: signatureResult
)
}
guard let signature = try? Ed25519.sign(message, with: targetKeyPair) else {
SNLog("Failed to sign open group message.")
// Otherwise fall back to sign using the unblinded key
guard let signatureResult: Bytes = dependencies.sign.signature(message: messageBytes, secretKey: userEdKeyPair.secretKey) else {
return nil
}
return (message, signature)
}
/// Sign a blinded message request to be sent to a users inbox via SOGS v4
private static func sign(message: Data, to blindedSessionId: String, on serverName: String, with serverPublicKey: String, using dependencies: Dependencies = Dependencies()) -> Data? {
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { return nil }
guard let blindedKeyPair: BlindedECKeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else {
return nil
}
guard let blindedRecipientPublicKey: Data = String(blindedSessionId.suffix(from: blindedSessionId.index(blindedSessionId.startIndex, offsetBy: IdPrefix.blinded.rawValue.count))).dataFromHex() else {
return nil
}
/// Generate the sharedSecret by "a kB || kA || kB" where
/// a, A are the users private and public keys respectively,
/// kA is the users blinded public key
/// kB is the recipients blinded public key
let maybeSharedSecret: Data? = dependencies.sodium
.sharedEdSecret(userEdKeyPair.secretKey, blindedRecipientPublicKey.bytes)?
.appending(blindedKeyPair.publicKey.bytes)
.appending(blindedRecipientPublicKey.bytes)
guard let sharedSecret: Data = maybeSharedSecret else { return nil }
guard let intermediateHash: Bytes = dependencies.genericHash.hash(message: sharedSecret.bytes) else { return nil }
/// Generate the inner message by "message || A" where
/// A is the sender's ed25519 master pubkey (**not** kA blinded pubkey)
let innerMessage: Bytes = (message.bytes + userEdKeyPair.publicKey)
guard let (ciphertext, nonce) = dependencies.aeadXChaCha20Poly1305Ietf.encrypt(message: innerMessage, secretKey: intermediateHash) else {
return nil
}
/// Generate the final data by "b'\x00' + ciphertext + nonce"
let finalData: Bytes = [0] + ciphertext + nonce
return Data(finalData)
return (
publicKey: IdPrefix.unblinded.hexEncodedPublicKey(for: userEdKeyPair.publicKey),
signature: signatureResult
)
}
/// Sign a request to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities)
@ -666,13 +632,9 @@ public final class OpenGroupAPI: NSObject {
let method: String = (request.httpMethod ?? "GET")
let timestamp: Int = Int(floor(dependencies.date.timeIntervalSince1970))
let nonce: Data = Data(dependencies.nonceGenerator.nonce())
let server: Server? = dependencies.storage.getOpenGroupServer(name: serverName)
let userPublicKeyHex: String
let signatureBytes: Bytes
guard let serverPublicKeyData: Data = serverPublicKey.dataFromHex() else { return nil }
guard let timestampBytes: Bytes = "\(timestamp)".data(using: .ascii)?.bytes else { return nil }
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { return nil }
/// Get a hash of any body content
let bodyHash: Bytes? = {
@ -693,51 +655,24 @@ public final class OpenGroupAPI: NSObject {
/// `Method`
/// `Path`
/// `Body` is a Blake2b hash of the data (if there is a body)
let signatureMessageBytes: Bytes = serverPublicKeyData.bytes
let messageBytes: Bytes = serverPublicKeyData.bytes
.appending(nonce.bytes)
.appending(timestampBytes)
.appending(method.bytes)
.appending(path.bytes)
.appending(bodyHash ?? [])
// Determine if we want to sign using standard or blinded keys based on the server capabilities (assume
// unblinded if we have none)
// TODO: Remove this (blinding will be required)
if server?.capabilities.capabilities.contains(.blinding) == true {
// TODO: More testing of this blinded id signing (though it seems to be working!!!)
guard let blindedKeyPair: Box.KeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else {
return nil
}
userPublicKeyHex = IdPrefix.blinded.hexEncodedPublicKey(for: blindedKeyPair.publicKey)
guard let signatureResult: Bytes = Sodium().sogsSignature(message: signatureMessageBytes, secretKey: userEdKeyPair.secretKey, blindedSecretKey: blindedKeyPair.secretKey, blindedPublicKey: blindedKeyPair.publicKey) else {
return nil
}
signatureBytes = signatureResult
}
else {
userPublicKeyHex = IdPrefix.unblinded.hexEncodedPublicKey(for: userEdKeyPair.publicKey)
// TODO: shift this to dependencies
guard let signatureResult: Bytes = Sodium().sign.signature(message: signatureMessageBytes, secretKey: userEdKeyPair.secretKey) else {
return nil
}
signatureBytes = signatureResult
/// Sign the above message
guard let signResult: (publicKey: String, signature: Bytes) = sign(messageBytes, for: serverName, using: dependencies) else {
return nil
}
print("RAWR X-SOGS-Pubkey: \(userPublicKeyHex)")
print("RAWR X-SOGS-Timestamp: \(timestamp)")
print("RAWR X-SOGS-Nonce: \(nonce.base64EncodedString())")
print("RAWR X-SOGS-Signature: \(signatureBytes.toBase64())")
updatedRequest.allHTTPHeaderFields = (request.allHTTPHeaderFields ?? [:])
.updated(with: [
Header.sogsPubKey.rawValue: userPublicKeyHex,
Header.sogsPubKey.rawValue: signResult.publicKey,
Header.sogsTimestamp.rawValue: "\(timestamp)",
Header.sogsNonce.rawValue: nonce.base64EncodedString(),
Header.sogsSignature.rawValue: signatureBytes.toBase64()
Header.sogsSignature.rawValue: signResult.signature.toBase64()
])
return updatedRequest
@ -756,7 +691,7 @@ public final class OpenGroupAPI: NSObject {
urlRequest.httpBody = request.body
if request.useOnionRouting {
guard let publicKey = SNMessagingKitConfiguration.shared.storage.getOpenGroupPublicKey(for: request.server) else {
guard let publicKey = dependencies.storage.getOpenGroupPublicKey(for: request.server) else {
return Promise(error: Error.noPublicKey)
}

View File

@ -10,16 +10,21 @@ extension OpenGroupAPI {
let storage: SessionMessagingKitStorageProtocol
let sodium: SodiumType
let aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType
let sign: SignType
let genericHash: GenericHashType
let ed25519: Ed25519Type.Type
let nonceGenerator: NonceGenerator16ByteType
let date: Date
public init(
api: OnionRequestAPIType.Type = OnionRequestAPI.self,
storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage,
// TODO: Shift the next 3 to be abstracted behind a single "signing" class?
sodium: SodiumType = Sodium(),
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
sign: SignType? = nil,
genericHash: GenericHashType? = nil,
ed25519: Ed25519Type.Type = Ed25519.self,
nonceGenerator: NonceGenerator16ByteType = NonceGenerator16Byte(),
date: Date = Date()
) {
@ -27,7 +32,9 @@ extension OpenGroupAPI {
self.storage = storage
self.sodium = sodium
self.aeadXChaCha20Poly1305Ietf = (aeadXChaCha20Poly1305Ietf ?? sodium.getAeadXChaCha20Poly1305Ietf())
self.sign = (sign ?? sodium.getSign())
self.genericHash = (genericHash ?? sodium.getGenericHash())
self.ed25519 = ed25519
self.nonceGenerator = nonceGenerator
self.date = date
}
@ -39,7 +46,9 @@ extension OpenGroupAPI {
storage: SessionMessagingKitStorageProtocol? = nil,
sodium: SodiumType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
sign: SignType? = nil,
genericHash: GenericHashType? = nil,
ed25519: Ed25519Type.Type? = nil,
nonceGenerator: NonceGenerator16ByteType? = nil,
date: Date? = nil
) -> Dependencies {
@ -48,7 +57,9 @@ extension OpenGroupAPI {
storage: (storage ?? self.storage),
sodium: (sodium ?? self.sodium),
aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self.aeadXChaCha20Poly1305Ietf),
sign: (sign ?? self.sign),
genericHash: (genericHash ?? self.genericHash),
ed25519: (ed25519 ?? self.ed25519),
nonceGenerator: (nonceGenerator ?? self.nonceGenerator),
date: (date ?? self.date)
)

View File

@ -2,20 +2,32 @@
import Foundation
import Sodium
import Curve25519Kit
public protocol SodiumType {
func getGenericHash() -> GenericHashType
func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType
func getSign() -> SignType
func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair?
func sharedEdSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Sodium.SharedSecret?
func sharedSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Sodium.SharedSecret?
func sogsSignature(message: Bytes, secretKey: Bytes, blindedSecretKey ka: Bytes, blindedPublicKey kA: Bytes) -> Bytes?
func sharedEdSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Bytes?
}
public protocol AeadXChaCha20Poly1305IetfType {
func encrypt(message: Bytes, secretKey: Aead.XChaCha20Poly1305Ietf.Key, additionalData: Bytes?) -> (authenticatedCipherText: Bytes, nonce: Aead.XChaCha20Poly1305Ietf.Nonce)?
}
public protocol Ed25519Type {
static func verifySignature(_ signature: Data, publicKey: Data, data: Data) throws -> Bool
}
public protocol SignType {
func signature(message: Bytes, secretKey: Bytes) -> Bytes?
func verify(message: Bytes, publicKey: Bytes, signature: Bytes) -> Bool
}
public protocol GenericHashType {
func hash(message: Bytes, key: Bytes?) -> Bytes?
func hash(message: Bytes, outputLength: Int) -> Bytes?
@ -42,6 +54,7 @@ extension GenericHashType {
extension Sodium: SodiumType {
public func getGenericHash() -> GenericHashType { return genericHash }
public func getSign() -> SignType { return sign }
public func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return aead.xchacha20poly1305ietf }
public func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair) -> Box.KeyPair? {
@ -50,4 +63,6 @@ extension Sodium: SodiumType {
}
extension Aead.XChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType {}
extension Sign: SignType {}
extension GenericHash: GenericHashType {}
extension Ed25519: Ed25519Type {}

View File

@ -1,6 +1,7 @@
import PromiseKit
import SessionSnodeKit
import SessionUtilitiesKit
import Sodium
@objc(SNMessageSender)
public final class MessageSender : NSObject {
@ -277,22 +278,34 @@ public final class MessageSender : NSObject {
// MARK: - Open Groups
internal static func sendToOpenGroupDestination(_ destination: Message.Destination, message: Message, using transaction: Any) -> Promise<Void> {
internal static func sendToOpenGroupDestination(_ destination: Message.Destination, message: Message, using transaction: Any, dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise<Void> {
let (promise, seal) = Promise<Void>.pending()
let storage = SNMessagingKitConfiguration.shared.storage
let transaction = transaction as! YapDatabaseReadWriteTransaction
// Set the timestamp, sender and recipient
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set
message.sentTimestamp = NSDate.millisecondTimestamp()
message.sentTimestamp = UInt64(dependencies.date.timeIntervalSince1970 * 1000) // Should be in ms
}
guard let threadId: String = message.threadID, let openGroup = Storage.shared.getOpenGroup(for: threadId) else {
guard let threadId: String = message.threadID, let openGroup = dependencies.storage.getOpenGroup(for: threadId) else {
preconditionFailure()
}
// TODO: Check if blinding is enabled on this server?
if let userDerivedKey: ECKeyPair = try? OWSIdentityManager.shared().identityKeyPair()?.convert(to: .blinded, with: openGroup.publicKey) {
message.sender = userDerivedKey.hexEncodedPublicKey
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { preconditionFailure() }
let server: OpenGroupAPI.Server? = dependencies.storage.getOpenGroupServer(name: openGroup.server)
if server?.capabilities.capabilities.contains(.blinding) == true {
guard let serverPublicKey = dependencies.storage.getOpenGroupPublicKey(for: openGroup.server) else {
preconditionFailure()
}
guard let blindedKeyPair: Box.KeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else {
preconditionFailure()
}
message.sender = IdPrefix.blinded.hexEncodedPublicKey(for: blindedKeyPair.publicKey)
}
else {
message.sender = IdPrefix.unblinded.hexEncodedPublicKey(for: userEdKeyPair.publicKey)
}
switch destination {
@ -332,12 +345,12 @@ public final class MessageSender : NSObject {
}
// Attach the user's profile
guard let name = storage.getUser()?.name else {
guard let name = dependencies.storage.getUser()?.name else {
handleFailure(with: Error.noUsername, using: transaction)
return promise
}
if let profileKey = storage.getUser()?.profileEncryptionKey?.keyData, let profilePictureURL = storage.getUser()?.profilePictureURL {
if let profileKey = dependencies.storage.getUser()?.profileEncryptionKey?.keyData, let profilePictureURL = dependencies.storage.getUser()?.profilePictureURL {
message.profile = VisibleMessage.Profile(displayName: name, profileKey: profileKey, profilePictureURL: profilePictureURL)
}
else {
@ -379,15 +392,15 @@ public final class MessageSender : NSObject {
.done(on: DispatchQueue.global(qos: .userInitiated)) { responseInfo, data in
message.openGroupServerMessageID = given(data.seqNo) { UInt64($0) }
Storage.shared.write { transaction in
dependencies.storage.write { transaction in
MessageSender.handleSuccessfulMessageSend(message, to: destination, serverTimestamp: UInt64(floor(data.posted)), using: transaction)
seal.fulfill(())
}
}
.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
storage.write(with: { transaction in
dependencies.storage.write { transaction in
handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction)
}, completion: { })
}
}
return promise

View File

@ -0,0 +1,23 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
// MARK: - Decoding
extension OpenGroupAPI.Dependencies {
static let userInfoKey: CodingUserInfoKey = CodingUserInfoKey(rawValue: "io.oxen.dependencies.codingOptions")!
}
public extension Data {
func decoded<T: Decodable>(as type: T.Type, customError: Error? = nil, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) throws -> T {
do {
let decoder: JSONDecoder = JSONDecoder()
decoder.userInfo = [ OpenGroupAPI.Dependencies.userInfoKey: dependencies ]
return try decoder.decode(type, from: self)
}
catch let error {
throw (customError ?? error)
}
}
}

View File

@ -1,34 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Curve25519Kit
import SessionUtilitiesKit
import Sodium
public extension ECKeyPair {
func convert(to targetPrefix: IdPrefix, with otherKey: String, using sodium: Sodium = Sodium()) throws -> ECKeyPair? {
guard let publicKeyPrefix: IdPrefix = IdPrefix(with: hexEncodedPublicKey) else { return nil }
switch (publicKeyPrefix, targetPrefix) {
case (.standard, .blinded): // Only support standard -> blinded conversions
// TODO: Figure out why this is broken...
// guard let otherPubKeyData: Data = otherKey.data(using: .utf8) else { return nil }
guard let otherPubKeyData: Data = otherKey.dataFromHex() else { return nil }
guard let otherPubKeyHashBytes: Bytes = sodium.genericHash.hash(message: [UInt8](otherPubKeyData)) else {
return nil
}
guard let blindedPublicKey: Sodium.SharedSecret = sodium.sharedSecret(otherPubKeyHashBytes, [UInt8](publicKey)) else {
return nil
}
guard let blindedPrivateKey: Sodium.SharedSecret = sodium.sharedSecret(otherPubKeyHashBytes, [UInt8](privateKey)) else {
return nil
}
return try BlindedECKeyPair(publicKeyData: blindedPublicKey, privateKeyData: blindedPrivateKey)
case (.standard, .standard): return self
case (.blinded, .blinded): return self
default: return nil
}
}
}

View File

@ -5,21 +5,21 @@ import PromiseKit
import SessionSnodeKit
extension Promise where T == Data {
func decoded<R: Decodable>(as type: R.Type, on queue: DispatchQueue? = nil, error: Error? = nil) -> Promise<R> {
func decoded<R: Decodable>(as type: R.Type, on queue: DispatchQueue? = nil, error: Error? = nil, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise<R> {
self.map(on: queue) { data -> R in
try data.decoded(as: type, customError: error)
try data.decoded(as: type, customError: error, using: dependencies)
}
}
}
extension Promise where T == (OnionRequestResponseInfoType, Data?) {
func decoded<R: Decodable>(as type: R.Type, on queue: DispatchQueue? = nil, error: Error? = nil) -> Promise<(OnionRequestResponseInfoType, R)> {
func decoded<R: Decodable>(as type: R.Type, on queue: DispatchQueue? = nil, error: Error? = nil, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise<(OnionRequestResponseInfoType, R)> {
self.map(on: queue) { responseInfo, maybeData -> (OnionRequestResponseInfoType, R) in
guard let data: Data = maybeData else {
throw OpenGroupAPI.Error.parsingFailed
}
return (responseInfo, try data.decoded(as: type, customError: error))
return (responseInfo, try data.decoded(as: type, customError: error, using: dependencies))
}
}
}

View File

@ -41,14 +41,13 @@ extension Sign {
}
extension Sodium {
public typealias SharedSecret = Data
private static let scalarLength: Int = Int(crypto_core_ed25519_scalarbytes()) // 32
private static let noClampLength: Int = Int(crypto_scalarmult_ed25519_bytes()) // 32
private static let scalarMultLength: Int = Int(crypto_scalarmult_bytes()) // 32
private static let publicKeyLength: Int = Int(crypto_scalarmult_bytes()) // 32
private static let secretKeyLength: Int = Int(crypto_sign_secretkeybytes()) // 64
/// Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair`
public func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair? {
guard edKeyPair.publicKey.count == Sodium.publicKeyLength && edKeyPair.secretKey.count == Sodium.secretKeyLength else {
return nil
@ -171,7 +170,8 @@ extension Sodium {
return (Data(bytes: sig_RPtr, count: Sodium.noClampLength).bytes + Data(bytes: sig_sPtr, count: Sodium.scalarLength).bytes)
}
public func sharedEdSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> SharedSecret? {
// TODO: Determine if we still need this? (To generate the `kB` value for the `/inbox` API????)
public func sharedEdSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Bytes? {
let sharedSecretPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.noClampLength)
let result = secondKeyBytes.withUnsafeBytes { (secondKeyPtr: UnsafeRawBufferPointer) -> Int32 in
return firstKeyBytes.withUnsafeBytes { (firstKeyPtr: UnsafeRawBufferPointer) -> Int32 in
@ -188,33 +188,7 @@ extension Sodium {
guard result == 0 else { return nil }
return Data(bytes: sharedSecretPtr, count: Sodium.scalarMultLength)
}
public func sharedSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> SharedSecret? {
guard firstKeyBytes.count == Sodium.publicKeyLength && secondKeyBytes.count == Sodium.publicKeyLength else {
return nil
}
let sharedSecretPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarMultLength)
let result = secondKeyBytes.withUnsafeBytes { (secondKeyPtr: UnsafeRawBufferPointer) -> Int32 in
return firstKeyBytes.withUnsafeBytes { (firstKeyPtr: UnsafeRawBufferPointer) -> Int32 in
guard let firstKeyBaseAddress: UnsafePointer<UInt8> = firstKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
}
guard let secondKeyBaseAddress: UnsafePointer<UInt8> = secondKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
}
//crypto_sign_ed25519_publickeybytes
//crypto_scalarmult_curve25519(<#T##q: UnsafeMutablePointer<UInt8>##UnsafeMutablePointer<UInt8>#>, <#T##n: UnsafePointer<UInt8>##UnsafePointer<UInt8>#>, <#T##p: UnsafePointer<UInt8>##UnsafePointer<UInt8>#>)
return crypto_scalarmult(sharedSecretPtr, firstKeyBaseAddress, secondKeyBaseAddress)
}
}
guard result == 0 else { return nil }
return Data(bytes: sharedSecretPtr, count: Sodium.scalarMultLength)
return Data(bytes: sharedSecretPtr, count: Sodium.scalarMultLength).bytes
}
}

View File

@ -67,15 +67,28 @@ class OpenGroupAPITests: XCTestCase {
}
var testStorage: TestStorage!
var testSodium: TestSodium!
var testAeadXChaCha20Poly1305Ietf: TestAeadXChaCha20Poly1305Ietf!
var testGenericHash: TestGenericHash!
var testSign: TestSign!
var dependencies: OpenGroupAPI.Dependencies!
// MARK: - Configuration
override func setUpWithError() throws {
testStorage = TestStorage()
testSodium = TestSodium()
testAeadXChaCha20Poly1305Ietf = TestAeadXChaCha20Poly1305Ietf()
testGenericHash = TestGenericHash()
testSign = TestSign()
dependencies = OpenGroupAPI.Dependencies(
api: TestApi.self,
storage: testStorage,
sodium: testSodium,
aeadXChaCha20Poly1305Ietf: testAeadXChaCha20Poly1305Ietf,
sign: testSign,
genericHash: testGenericHash,
ed25519: TestEd25519.self,
nonceGenerator: TestNonceGenerator(),
date: Date(timeIntervalSince1970: 1234567890)
)
@ -100,6 +113,18 @@ class OpenGroupAPITests: XCTestCase {
publicKeyData: Data.data(fromHex: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")!,
privateKeyData: Data.data(fromHex: "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4")!
)
testStorage.mockData[.userEdKeyPair] = Box.KeyPair(
publicKey: Data.data(fromHex: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")!.bytes,
secretKey: Data.data(fromHex: "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4")!.bytes
)
testGenericHash.mockData[.hashOutputLength] = []
testSodium.mockData[.blindedKeyPair] = Box.KeyPair(
publicKey: Data.data(fromHex: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")!.bytes,
secretKey: Data.data(fromHex: "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4")!.bytes
)
testSodium.mockData[.sogsSignature] = "TestSogsSignature".bytes
testSign.mockData[.signature] = "TestSignature".bytes
}
override func tearDownWithError() throws {
@ -415,12 +440,16 @@ class OpenGroupAPITests: XCTestCase {
// MARK: - Authentication
func testItSignsTheRequestCorrectly() throws {
func testItSignsTheUnblindedRequestCorrectly() throws {
class LocalTestApi: TestApi {
override class var mockResponse: Data? {
return try! JSONEncoder().encode([OpenGroupAPI.Room]())
}
}
testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server(
name: "testServer",
capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: [])
)
dependencies = dependencies.with(api: LocalTestApi.self)
var response: (OnionRequestResponseInfoType, [OpenGroupAPI.Room])? = nil
@ -445,14 +474,74 @@ class OpenGroupAPITests: XCTestCase {
expect(requestData?.server).to(equal("testServer"))
expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"))
expect(requestData?.headers).to(haveCount(4))
expect(requestData?.headers[Header.sogsPubKey.rawValue]).to(equal("057aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"))
expect(requestData?.headers[Header.sogsPubKey.rawValue]).to(equal("007aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"))
expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890"))
expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg=="))
expect(requestData?.headers[Header.sogsSignature.rawValue]).to(equal("fxqLy5ZDWCsLQpwLw0Dax+4xe7cG2vPRk1NlHORIm0DPd3o9UA24KLZY"))
expect(requestData?.headers[Header.sogsSignature.rawValue]).to(equal("TestSignature".bytes.toBase64()))
}
func testItSignsTheBlindedRequestCorrectly() throws {
class LocalTestApi: TestApi {
override class var mockResponse: Data? {
return try! JSONEncoder().encode([OpenGroupAPI.Room]())
}
}
testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server(
name: "testServer",
capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs, .blinding], missing: [])
)
dependencies = dependencies.with(api: LocalTestApi.self)
var response: (OnionRequestResponseInfoType, [OpenGroupAPI.Room])? = nil
var error: Error? = nil
OpenGroupAPI.rooms(for: "testServer", using: dependencies)
.get { result in response = result }
.catch { requestError in error = requestError }
.retainUntilComplete()
expect(response)
.toEventuallyNot(
beNil(),
timeout: .milliseconds(100)
)
expect(error?.localizedDescription).to(beNil())
// Validate signature headers
let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData
expect(requestData?.urlString).to(equal("testServer/rooms"))
expect(requestData?.httpMethod).to(equal("GET"))
expect(requestData?.server).to(equal("testServer"))
expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"))
expect(requestData?.headers).to(haveCount(4))
expect(requestData?.headers[Header.sogsPubKey.rawValue]).to(equal("157aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"))
expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890"))
expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg=="))
expect(requestData?.headers[Header.sogsSignature.rawValue]).to(equal("TestSogsSignature".bytes.toBase64()))
}
func testItFailsToSignIfThereIsNoUserEdKeyPair() throws {
testStorage.mockData[.userEdKeyPair] = nil
var response: Any? = nil
var error: Error? = nil
OpenGroupAPI.rooms(for: "testServer", using: dependencies)
.get { result in response = result }
.catch { requestError in error = requestError }
.retainUntilComplete()
expect(error?.localizedDescription)
.toEventually(
equal(OpenGroupAPI.Error.signingFailed.localizedDescription),
timeout: .milliseconds(100)
)
expect(response).to(beNil())
}
func testItFailsToSignIfTheServerPublicKeyIsInvalid() throws {
testStorage.mockData[.openGroupPublicKeys] = ["testServer": ""]
testStorage.mockData[.openGroupPublicKeys] = [:]
var response: Any? = nil
var error: Error? = nil
@ -464,44 +553,32 @@ class OpenGroupAPITests: XCTestCase {
expect(error?.localizedDescription)
.toEventually(
equal(OpenGroupAPI.Error.signingFailed.localizedDescription),
equal(OpenGroupAPI.Error.noPublicKey.localizedDescription),
timeout: .milliseconds(100)
)
expect(response).to(beNil())
}
func testItFailsToSignIfThereIsNoUserKeyPair() throws {
testStorage.mockData[.userKeyPair] = nil
var response: Any? = nil
var error: Error? = nil
OpenGroupAPI.rooms(for: "testServer", using: dependencies)
.get { result in response = result }
.catch { requestError in error = requestError }
.retainUntilComplete()
expect(error?.localizedDescription)
.toEventually(
equal(OpenGroupAPI.Error.signingFailed.localizedDescription),
timeout: .milliseconds(100)
)
expect(response).to(beNil())
}
func testItFailsToSignIfTheSharedSecretDoesNotGetGenerated() throws {
func testItFailsToSignIfBlindedAndTheBlindedKeyDoesNotGetGenerated() throws {
class InvalidSodium: SodiumType {
func getGenericHash() -> GenericHashType { return Sodium().genericHash }
func sharedSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Sodium.SharedSecret? { return nil }
func sharedEdSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Sodium.SharedSecret? { return nil }
func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return Sodium().aead.xchacha20poly1305ietf }
func getSign() -> SignType { return Sodium().sign }
func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair? {
return nil
}
func sogsSignature(message: Bytes, secretKey: Bytes, blindedSecretKey ka: Bytes, blindedPublicKey kA: Bytes) -> Bytes? {
return nil
}
func sharedEdSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Bytes? { return nil }
}
testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server(
name: "testServer",
capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs, .blinding], missing: [])
)
dependencies = dependencies.with(sodium: InvalidSodium())
var response: Any? = nil
@ -521,15 +598,29 @@ class OpenGroupAPITests: XCTestCase {
expect(response).to(beNil())
}
func testItFailsToSignIfTheIntermediateHashDoesNotGetGenerated() throws {
class InvalidGenericHash: GenericHashType {
func hash(message: Bytes, key: Bytes?) -> Bytes? { return nil }
func hashSaltPersonal(message: Bytes, outputLength: Int, key: Bytes?, salt: Bytes, personal: Bytes) -> Bytes? {
func testItFailsToSignIfBlindedAndTheSogsSignatureDoesNotGetGenerated() throws {
class InvalidSodium: SodiumType {
func getGenericHash() -> GenericHashType { return Sodium().genericHash }
func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return Sodium().aead.xchacha20poly1305ietf }
func getSign() -> SignType { return Sodium().sign }
func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair? {
return Box.KeyPair(
publicKey: Data.data(fromHex: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")!.bytes,
secretKey: Data.data(fromHex: "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4")!.bytes
)
}
func sogsSignature(message: Bytes, secretKey: Bytes, blindedSecretKey ka: Bytes, blindedPublicKey kA: Bytes) -> Bytes? {
return nil
}
func sharedEdSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Bytes? { return nil }
}
dependencies = dependencies.with(genericHash: InvalidGenericHash())
testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server(
name: "testServer",
capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs, .blinding], missing: [])
)
dependencies = dependencies.with(sodium: InvalidSodium())
var response: Any? = nil
var error: Error? = nil
@ -548,22 +639,16 @@ class OpenGroupAPITests: XCTestCase {
expect(response).to(beNil())
}
func testItFailsToSignIfTheSecretHashDoesNotGetGenerated() throws {
class InvalidSecondGenericHash: GenericHashType {
static var didSucceedOnce: Bool = false
func hash(message: Bytes, key: Bytes?) -> Bytes? { return nil }
func hashSaltPersonal(message: Bytes, outputLength: Int, key: Bytes?, salt: Bytes, personal: Bytes) -> Bytes? {
if !InvalidSecondGenericHash.didSucceedOnce {
InvalidSecondGenericHash.didSucceedOnce = true
return Data().bytes
}
return nil
}
func testItFailsToSignIfUnblindedAndTheSignatureDoesNotGetGenerated() throws {
class InvalidSign: SignType {
func signature(message: Bytes, secretKey: Bytes) -> Bytes? { return nil }
func verify(message: Bytes, publicKey: Bytes, signature: Bytes) -> Bool { return false }
}
dependencies = dependencies.with(genericHash: InvalidSecondGenericHash())
testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server(
name: "testServer",
capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: [])
)
dependencies = dependencies.with(sign: InvalidSign())
var response: Any? = nil
var error: Error? = nil

View File

@ -7,3 +7,9 @@ protocol Mockable {
var mockData: [Key: Any] { get }
}
protocol StaticMockable {
associatedtype Key: Hashable
static var mockData: [Key: Any] { get }
}

View File

@ -0,0 +1,25 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Sodium
@testable import SessionMessagingKit
class TestAeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType, Mockable {
// MARK: - Mockable
enum DataKey: Hashable {
case encrypt
}
typealias Key = DataKey
var mockData: [DataKey: Any] = [:]
// MARK: - SignType
func encrypt(message: Bytes, secretKey: Aead.XChaCha20Poly1305Ietf.Key, additionalData: Bytes?) -> (authenticatedCipherText: Bytes, nonce: Aead.XChaCha20Poly1305Ietf.Nonce)? {
return (mockData[.encrypt] as? (authenticatedCipherText: Bytes, nonce: Aead.XChaCha20Poly1305Ietf.Nonce))
}
}

View File

@ -0,0 +1,25 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Sodium
@testable import SessionMessagingKit
class TestEd25519: Ed25519Type, StaticMockable {
// MARK: - Mockable
enum DataKey: Hashable {
case verifySignature(signature: Data, publicKey: Data, data: Data) // TODO: Test the uniqueness of this
}
typealias Key = DataKey
static var mockData: [DataKey: Any] = [:]
// MARK: - SignType
static func verifySignature(_ signature: Data, publicKey: Data, data: Data) throws -> Bool {
return (mockData[.verifySignature(signature: signature, publicKey: publicKey, data: data)] as! Bool)
}
}

View File

@ -0,0 +1,35 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Sodium
@testable import SessionMessagingKit
class TestGenericHash: GenericHashType, Mockable {
// MARK: - Mockable
enum DataKey: Hashable {
case hash
case hashOutputLength
case hashSaltPersonal
}
typealias Key = DataKey
var mockData: [DataKey: Any] = [:]
// MARK: - SignType
func hash(message: Bytes, key: Bytes?) -> Bytes? {
return (mockData[.hash] as? Bytes)
}
func hash(message: Bytes, outputLength: Int) -> Bytes? {
return (mockData[.hashOutputLength] as? Bytes)
}
func hashSaltPersonal(message: Bytes, outputLength: Int, key: Bytes?, salt: Bytes, personal: Bytes) -> Bytes? {
return (mockData[.hashSaltPersonal] as? Bytes)
}
}

View File

@ -0,0 +1,30 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Sodium
@testable import SessionMessagingKit
class TestSign: SignType, Mockable {
// MARK: - Mockable
enum DataKey: Hashable {
case signature
case verify
}
typealias Key = DataKey
var mockData: [DataKey: Any] = [:]
// MARK: - SignType
func signature(message: Bytes, secretKey: Bytes) -> Bytes? {
return (mockData[.signature] as? Bytes)
}
func verify(message: Bytes, publicKey: Bytes, signature: Bytes) -> Bool {
return (mockData[.verify] as! Bool)
}
}

View File

@ -0,0 +1,46 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Sodium
@testable import SessionMessagingKit
class TestSodium: SodiumType, Mockable {
// MARK: - Mockable
enum DataKey: Hashable {
case genericHash
case aeadXChaCha20Poly1305Ietf
case sign
case blindedKeyPair
case sogsSignature
case sharedEdSecret
}
typealias Key = DataKey
var mockData: [DataKey: Any] = [:]
// MARK: - SodiumType
func getGenericHash() -> GenericHashType { return (mockData[.genericHash] as! GenericHashType) }
func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType {
return (mockData[.aeadXChaCha20Poly1305Ietf] as! AeadXChaCha20Poly1305IetfType)
}
func getSign() -> SignType { return (mockData[.sign] as! SignType) }
func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair? {
return (mockData[.blindedKeyPair] as? Box.KeyPair)
}
func sogsSignature(message: Bytes, secretKey: Bytes, blindedSecretKey ka: Bytes, blindedPublicKey kA: Bytes) -> Bytes? {
return (mockData[.sogsSignature] as? Bytes)
}
func sharedEdSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Bytes? {
return (mockData[.sharedEdSecret] as? Bytes)
}
}

View File

@ -13,6 +13,7 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable {
case allOpenGroups
case openGroupPublicKeys
case userKeyPair
case userEdKeyPair
case openGroup
case openGroupServer
case openGroupImage
@ -43,7 +44,7 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable {
func getUserPublicKey() -> String? { return nil }
func getUserKeyPair() -> ECKeyPair? { return (mockData[.userKeyPair] as? ECKeyPair) }
func getUserED25519KeyPair() -> Box.KeyPair? { return nil }
func getUserED25519KeyPair() -> Box.KeyPair? { return (mockData[.userEdKeyPair] as? Box.KeyPair) }
func getUser() -> Contact? { return nil }
func getAllContacts() -> Set<Contact> { return Set() }

View File

@ -33,16 +33,3 @@ public extension Data {
return result as NSData
}
}
// MARK: - Decoding
public extension Data {
func decoded<T: Decodable>(as type: T.Type, customError: Error? = nil) throws -> T {
do {
return try JSONDecoder().decode(type, from: self)
}
catch let error {
throw (customError ?? error)
}
}
}