Added more unit tests

Fixed a possible divide by zero error
Cleaned up some of the id blinding methods (ie. removing handling for impossible error states)
Added unit tests for the new Sodium methods (used for id blinding)
Added unit tests for some of the shared code
Added unit tests for the MessageSender+Encryption extension functions
Added unit tests for the MessageReceiver+Decryption extension functions
Updated the unit test key constants to be consistent with the SOGS auth-example keys for consistency
This commit is contained in:
Morgan Pretty 2022-03-18 16:39:25 +11:00
parent 2851d5e8c7
commit c44256b1d6
30 changed files with 2051 additions and 140 deletions

View File

@ -788,6 +788,15 @@
FD078E6027E2BB36000769AF /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E5F27E2BB36000769AF /* MockIdentityManager.swift */; };
FD0BA51B27CD88EC00CC6805 /* BlindedIdMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */; };
FD0BA51D27CDC34600CC6805 /* SOGSV4Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */; };
FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */; };
FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; };
FD3C906227E411AF00CD579F /* HeaderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906127E411AF00CD579F /* HeaderSpec.swift */; };
FD3C906427E4122F00CD579F /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906327E4122F00CD579F /* RequestSpec.swift */; };
FD3C906727E416AF00CD579F /* BlindedIdMappingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906627E416AF00CD579F /* BlindedIdMappingSpec.swift */; };
FD3C906A27E417CE00CD579F /* SodiumUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */; };
FD3C906D27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */; };
FD3C906F27E43E8700CD579F /* MockBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906E27E43E8700CD579F /* MockBox.swift */; };
FD3C907127E445E500CD579F /* MessageReceiverDecryptionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */; };
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; };
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; };
FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201D27B0D87C00FEA984 /* SessionId.swift */; };
@ -1938,6 +1947,15 @@
FD078E5F27E2BB36000769AF /* MockIdentityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockIdentityManager.swift; sourceTree = "<group>"; };
FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdMapping.swift; sourceTree = "<group>"; };
FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSV4Migration.swift; sourceTree = "<group>"; };
FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfoSpec.swift; sourceTree = "<group>"; };
FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = "<group>"; };
FD3C906127E411AF00CD579F /* HeaderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderSpec.swift; sourceTree = "<group>"; };
FD3C906327E4122F00CD579F /* RequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestSpec.swift; sourceTree = "<group>"; };
FD3C906627E416AF00CD579F /* BlindedIdMappingSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdMappingSpec.swift; sourceTree = "<group>"; };
FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SodiumUtilitiesSpec.swift; sourceTree = "<group>"; };
FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderEncryptionSpec.swift; sourceTree = "<group>"; };
FD3C906E27E43E8700CD579F /* MockBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBox.swift; sourceTree = "<group>"; };
FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceiverDecryptionSpec.swift; sourceTree = "<group>"; };
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = "<group>"; };
@ -3884,6 +3902,49 @@
path = Session;
sourceTree = "<group>";
};
FD3C905D27E410DB00CD579F /* Common Networking */ = {
isa = PBXGroup;
children = (
FD3C905E27E410EE00CD579F /* Models */,
FD3C906127E411AF00CD579F /* HeaderSpec.swift */,
FD3C906327E4122F00CD579F /* RequestSpec.swift */,
);
path = "Common Networking";
sourceTree = "<group>";
};
FD3C905E27E410EE00CD579F /* Models */ = {
isa = PBXGroup;
children = (
FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */,
);
path = Models;
sourceTree = "<group>";
};
FD3C906527E416A200CD579F /* Contacts */ = {
isa = PBXGroup;
children = (
FD3C906627E416AF00CD579F /* BlindedIdMappingSpec.swift */,
);
path = Contacts;
sourceTree = "<group>";
};
FD3C906827E417B100CD579F /* Utilities */ = {
isa = PBXGroup;
children = (
FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */,
);
path = Utilities;
sourceTree = "<group>";
};
FD3C906B27E43C2400CD579F /* Sending & Receiving */ = {
isa = PBXGroup;
children = (
FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */,
FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */,
);
path = "Sending & Receiving";
sourceTree = "<group>";
};
FD659ABE27A7648200F12C02 /* Message Requests */ = {
isa = PBXGroup;
children = (
@ -3924,6 +3985,7 @@
children = (
FD83B9C227CF33F7005E1583 /* ServerSpec.swift */,
FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */,
FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */,
FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */,
FDC2908627D7047F005DAE71 /* RoomSpec.swift */,
FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */,
@ -4038,7 +4100,11 @@
isa = PBXGroup;
children = (
FDC4389B27BA01E300C60D73 /* _TestUtilities */,
FD3C905D27E410DB00CD579F /* Common Networking */,
FD3C906527E416A200CD579F /* Contacts */,
FD3C906B27E43C2400CD579F /* Sending & Receiving */,
FDC4389827BA001800C60D73 /* Open Groups */,
FD3C906827E417B100CD579F /* Utilities */,
);
path = SessionMessagingKitTests;
sourceTree = "<group>";
@ -4061,9 +4127,10 @@
FD078E5F27E2BB36000769AF /* MockIdentityManager.swift */,
FDC4389C27BA01F000C60D73 /* MockStorage.swift */,
FD859EF327C2F49200510D0C /* MockSodium.swift */,
FD3C906E27E43E8700CD579F /* MockBox.swift */,
FD859EF927C2F5C500510D0C /* MockGenericHash.swift */,
FD859EF527C2F52C00510D0C /* MockSign.swift */,
FD859EF727C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift */,
FD859EF927C2F5C500510D0C /* MockGenericHash.swift */,
FD859EFB27C2F60700510D0C /* MockEd25519.swift */,
FD078E5927E29F09000769AF /* MockNonce16Generator.swift */,
FD078E5B27E29F78000769AF /* MockNonce24Generator.swift */,
@ -5673,20 +5740,25 @@
buildActionMask = 2147483647;
files = (
FDC290AC27DB0B1C005DAE71 /* MockedExtensions.swift in Sources */,
FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */,
FD078E5827E1B831000769AF /* TestIncomingMessage.swift in Sources */,
FD859EFA27C2F5C500510D0C /* MockGenericHash.swift in Sources */,
FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */,
FDC290AF27DFEE97005DAE71 /* TestTransaction.swift in Sources */,
FDC290B327DFF9F5005DAE71 /* TestOnionRequestAPI.swift in Sources */,
FDC2909127D709CA005DAE71 /* SOGSMessageSpec.swift in Sources */,
FD3C906A27E417CE00CD579F /* SodiumUtilitiesSpec.swift in Sources */,
FD078E6027E2BB36000769AF /* MockIdentityManager.swift in Sources */,
FD3C907127E445E500CD579F /* MessageReceiverDecryptionSpec.swift in Sources */,
FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */,
FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */,
FD078E4F27E175F1000769AF /* DependencyExtensions.swift in Sources */,
FDC2909C27D713D2005DAE71 /* SodiumProtocolsSpec.swift in Sources */,
FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */,
FD83B9C327CF33F7005E1583 /* ServerSpec.swift in Sources */,
FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */,
FDC2909A27D71376005DAE71 /* NonceGeneratorSpec.swift in Sources */,
FD3C906427E4122F00CD579F /* RequestSpec.swift in Sources */,
FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */,
FDC290A027D85826005DAE71 /* TestContactThread.swift in Sources */,
FD859EF827C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift in Sources */,
@ -5697,20 +5769,24 @@
FD859EFC27C2F60700510D0C /* MockEd25519.swift in Sources */,
FDC290A627D860CE005DAE71 /* Mock.swift in Sources */,
FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */,
FD3C906F27E43E8700CD579F /* MockBox.swift in Sources */,
FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */,
FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */,
FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */,
FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
FD3C906227E411AF00CD579F /* HeaderSpec.swift in Sources */,
FDC290B727E00FDB005DAE71 /* TestGroupThread.swift in Sources */,
FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */,
FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */,
FD859EF627C2F52C00510D0C /* MockSign.swift in Sources */,
FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */,
FD078E5A27E29F09000769AF /* MockNonce16Generator.swift in Sources */,
FD3C906D27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift in Sources */,
FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */,
FDC290A227D85890005DAE71 /* TestInteraction.swift in Sources */,
FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */,
FDC4389D27BA01F000C60D73 /* MockStorage.swift in Sources */,
FD3C906727E416AF00CD579F /* BlindedIdMappingSpec.swift in Sources */,
FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */,
FD078E5C27E29F78000769AF /* MockNonce24Generator.swift in Sources */,
);

View File

@ -96,3 +96,5 @@ struct Request<T: Encodable, Endpoint: EndpointType> {
return urlRequest
}
}
extension Request: Equatable where T: Equatable {}

View File

@ -21,12 +21,6 @@ public protocol OGMCacheType {
func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval
}
extension OGMCacheType {
func getTimeSinceLastOpen() -> TimeInterval {
return getTimeSinceLastOpen(using: Dependencies())
}
}
// MARK: - OpenGroupManager
@objc(SNOpenGroupManager)
@ -49,7 +43,7 @@ public final class OpenGroupManager: NSObject {
public var timeSinceLastPoll: [String: TimeInterval] = [:]
fileprivate var _timeSinceLastOpen: TimeInterval?
public func getTimeSinceLastOpen(using dependencies: Dependencies = Dependencies()) -> TimeInterval {
public func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval {
if let storedTimeSinceLastOpen: TimeInterval = _timeSinceLastOpen {
return storedTimeSinceLastOpen
}
@ -702,9 +696,10 @@ extension OpenGroupManager {
identityManager: IdentityManagerProtocol? = nil,
storage: SessionMessagingKitStorageProtocol? = nil,
sodium: SodiumType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
sign: SignType? = nil,
box: BoxType? = nil,
genericHash: GenericHashType? = nil,
sign: SignType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
ed25519: Ed25519Type? = nil,
nonceGenerator16: NonceGenerator16ByteType? = nil,
nonceGenerator24: NonceGenerator24ByteType? = nil,
@ -718,9 +713,10 @@ extension OpenGroupManager {
identityManager: identityManager,
storage: storage,
sodium: sodium,
aeadXChaCha20Poly1305Ietf: aeadXChaCha20Poly1305Ietf,
sign: sign,
box: box,
genericHash: genericHash,
sign: sign,
aeadXChaCha20Poly1305Ietf: aeadXChaCha20Poly1305Ietf,
ed25519: ed25519,
nonceGenerator16: nonceGenerator16,
nonceGenerator24: nonceGenerator24,

View File

@ -5,18 +5,19 @@ import Sodium
import Curve25519Kit
public protocol SodiumType {
func getBox() -> BoxType
func getGenericHash() -> GenericHashType
func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType
func getSign() -> SignType
func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType
func generateBlindingFactor(serverPublicKey: String) -> Bytes?
func generateBlindingFactor(serverPublicKey: String, genericHash: GenericHashType) -> Bytes?
func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair?
func sogsSignature(message: Bytes, secretKey: Bytes, blindedSecretKey ka: Bytes, blindedPublicKey kA: Bytes) -> Bytes?
func combineKeys(lhsKeyBytes: Bytes, rhsKeyBytes: Bytes) -> Bytes?
func sharedBlindedEncryptionKey(secretKey a: Bytes, otherBlindedPublicKey: Bytes, fromBlindedPublicKey kA: Bytes, toBlindedPublicKey kB: Bytes, genericHash: GenericHashType) -> Bytes?
func sessionId(_ sessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String) -> Bool
func sessionId(_ sessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String, genericHash: GenericHashType) -> Bool
}
public protocol AeadXChaCha20Poly1305IetfType {
@ -32,12 +33,9 @@ public protocol Ed25519Type {
func verifySignature(_ signature: Data, publicKey: Data, data: Data) throws -> Bool
}
public protocol SignType {
var PublicKeyBytes: Int { get }
func toX25519(ed25519PublicKey: Bytes) -> Bytes?
func signature(message: Bytes, secretKey: Bytes) -> Bytes?
func verify(message: Bytes, publicKey: Bytes, signature: Bytes) -> Bool
public protocol BoxType {
func seal(message: Bytes, recipientPublicKey: Bytes) -> Bytes?
func open(anonymousCipherText: Bytes, recipientPublicKey: Bytes, recipientSecretKey: Bytes) -> Bytes?
}
public protocol GenericHashType {
@ -46,8 +44,25 @@ public protocol GenericHashType {
func hashSaltPersonal(message: Bytes, outputLength: Int, key: Bytes?, salt: Bytes, personal: Bytes) -> Bytes?
}
public protocol SignType {
var Bytes: Int { get }
var PublicKeyBytes: Int { get }
func toX25519(ed25519PublicKey: Bytes) -> Bytes?
func signature(message: Bytes, secretKey: Bytes) -> Bytes?
func verify(message: Bytes, publicKey: Bytes, signature: Bytes) -> Bool
}
// MARK: - Default Values
extension GenericHashType {
func hash(message: Bytes) -> Bytes? { return hash(message: message, key: nil) }
func hashSaltPersonal(message: Bytes, outputLength: Int, salt: Bytes, personal: Bytes) -> Bytes? {
return hashSaltPersonal(message: message, outputLength: outputLength, key: nil, salt: salt, personal: personal)
}
}
extension AeadXChaCha20Poly1305IetfType {
func encrypt(message: Bytes, secretKey: Bytes, nonce: Bytes) -> Bytes? {
return encrypt(message: message, secretKey: secretKey, nonce: nonce, additionalData: nil)
@ -58,17 +73,10 @@ extension AeadXChaCha20Poly1305IetfType {
}
}
extension GenericHashType {
func hash(message: Bytes) -> Bytes? { return hash(message: message, key: nil) }
func hashSaltPersonal(message: Bytes, outputLength: Int, salt: Bytes, personal: Bytes) -> Bytes? {
return hashSaltPersonal(message: message, outputLength: outputLength, key: nil, salt: salt, personal: personal)
}
}
// MARK: - Conformance
extension Sodium: SodiumType {
public func getBox() -> BoxType { return box }
public func getGenericHash() -> GenericHashType { return genericHash }
public func getSign() -> SignType { return sign }
public func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return aead.xchacha20poly1305ietf }
@ -78,9 +86,10 @@ extension Sodium: SodiumType {
}
}
extension Aead.XChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType {}
extension Sign: SignType {}
extension Box: BoxType {}
extension GenericHash: GenericHashType {}
extension Sign: SignType {}
extension Aead.XChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType {}
struct Ed25519Wrapper: Ed25519Type {
func sign(data: Bytes, keyPair: ECKeyPair) throws -> Bytes? {

View File

@ -3,27 +3,40 @@ import SessionUtilitiesKit
import Sodium
extension MessageReceiver {
internal static func decryptWithSessionProtocol(ciphertext: Data, using x25519KeyPair: ECKeyPair) throws -> (plaintext: Data, senderX25519PublicKey: String) {
internal static func decryptWithSessionProtocol(ciphertext: Data, using x25519KeyPair: ECKeyPair, dependencies: Dependencies = Dependencies()) throws -> (plaintext: Data, senderX25519PublicKey: String) {
let recipientX25519PrivateKey = x25519KeyPair.privateKey
let recipientX25519PublicKey = Data(hex: x25519KeyPair.hexEncodedPublicKey.removingIdPrefixIfNeeded())
let sodium = Sodium()
let signatureSize = sodium.sign.Bytes
let ed25519PublicKeySize = sodium.sign.PublicKeyBytes
let signatureSize = dependencies.sign.Bytes
let ed25519PublicKeySize = dependencies.sign.PublicKeyBytes
// 1. ) Decrypt the message
guard let plaintextWithMetadata = sodium.box.open(anonymousCipherText: Bytes(ciphertext), recipientPublicKey: Box.PublicKey(Bytes(recipientX25519PublicKey)),
recipientSecretKey: Bytes(recipientX25519PrivateKey)), plaintextWithMetadata.count > (signatureSize + ed25519PublicKeySize) else { throw Error.decryptionFailed }
guard
let plaintextWithMetadata = dependencies.box.open(
anonymousCipherText: Bytes(ciphertext),
recipientPublicKey: Box.PublicKey(Bytes(recipientX25519PublicKey)),
recipientSecretKey: Bytes(recipientX25519PrivateKey)
),
plaintextWithMetadata.count > (signatureSize + ed25519PublicKeySize)
else {
throw Error.decryptionFailed
}
// 2. ) Get the message parts
let signature = Bytes(plaintextWithMetadata[plaintextWithMetadata.count - signatureSize ..< plaintextWithMetadata.count])
let senderED25519PublicKey = Bytes(plaintextWithMetadata[plaintextWithMetadata.count - (signatureSize + ed25519PublicKeySize) ..< plaintextWithMetadata.count - signatureSize])
let plaintext = Bytes(plaintextWithMetadata[0..<plaintextWithMetadata.count - (signatureSize + ed25519PublicKeySize)])
// 3. ) Verify the signature
let verificationData = plaintext + senderED25519PublicKey + recipientX25519PublicKey
let isValid = sodium.sign.verify(message: verificationData, publicKey: senderED25519PublicKey, signature: signature)
guard isValid else { throw Error.invalidSignature }
guard dependencies.sign.verify(message: verificationData, publicKey: senderED25519PublicKey, signature: signature) else {
throw Error.invalidSignature
}
// 4. ) Get the sender's X25519 public key
guard let senderX25519PublicKey = sodium.sign.toX25519(ed25519PublicKey: senderED25519PublicKey) else { throw Error.decryptionFailed }
guard let senderX25519PublicKey = dependencies.sign.toX25519(ed25519PublicKey: senderED25519PublicKey) else {
throw Error.decryptionFailed
}
return (Data(plaintext), SessionId(.standard, publicKey: senderX25519PublicKey).hexString)
}
@ -72,7 +85,7 @@ extension MessageReceiver {
])
/// Verify that the inner sender_edpk (A) yields the same outer kA we got with the message
guard let blindingFactor: Bytes = dependencies.sodium.generateBlindingFactor(serverPublicKey: openGroupPublicKey) else {
guard let blindingFactor: Bytes = dependencies.sodium.generateBlindingFactor(serverPublicKey: openGroupPublicKey, genericHash: dependencies.genericHash) else {
throw Error.invalidSignature
}
guard let sharedSecret: Bytes = dependencies.sodium.combineKeys(lhsKeyBytes: blindingFactor, rhsKeyBytes: sender_edpk) else {

View File

@ -18,7 +18,7 @@ extension MessageReceiver {
case let message as ExpirationTimerUpdate: handleExpirationTimerUpdate(message, using: transaction)
case let message as ConfigurationMessage: handleConfigurationMessage(message, using: transaction)
case let message as UnsendRequest: handleUnsendRequest(message, using: transaction)
case let message as MessageRequestResponse: handleMessageRequestResponse(message, using: transaction)
case let message as MessageRequestResponse: handleMessageRequestResponse(message, using: transaction, dependencies: dependencies)
case let message as VisibleMessage: try handleVisibleMessage(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction, dependencies: dependencies)
default: fatalError()
}
@ -839,7 +839,7 @@ extension MessageReceiver {
}
}
public static func handleMessageRequestResponse(_ message: MessageRequestResponse, using transaction: Any) {
public static func handleMessageRequestResponse(_ message: MessageRequestResponse, using transaction: Any, dependencies: Dependencies) {
let userPublicKey = getUserHexEncodedPublicKey()
var hadBlindedContact: Bool = false
var blindedThreadIds: [String] = []
@ -870,7 +870,7 @@ extension MessageReceiver {
// If the sessionId matches the blindedId then this thread needs to be converted to an un-blinded thread
guard let serverPublicKey: String = blindedThread.originalOpenGroupPublicKey else { continue }
guard Sodium().sessionId(senderId, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey) else { continue }
guard dependencies.sodium.sessionId(senderId, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey, genericHash: dependencies.genericHash) else { continue }
guard let blindedThreadId: String = blindedThread.uniqueId else { continue }
guard let view: YapDatabaseAutoViewTransaction = transaction.ext(TSMessageDatabaseViewExtensionName) as? YapDatabaseAutoViewTransaction else {
continue
@ -878,7 +878,7 @@ extension MessageReceiver {
// Cache the mapping
let mapping: BlindedIdMapping = BlindedIdMapping(blindedId: blindedId, sessionId: senderId, serverPublicKey: serverPublicKey)
Storage.shared.cacheBlindedIdMapping(mapping, using: transaction)
dependencies.storage.cacheBlindedIdMapping(mapping, using: transaction)
// Flag that we had a blinded contact and add the `blindedThreadId` to an array so we can remove
// them at the end of processing

View File

@ -2,22 +2,20 @@ import SessionUtilitiesKit
import Sodium
extension MessageSender {
internal static func encryptWithSessionProtocol(_ plaintext: Data, for recipientHexEncodedX25519PublicKey: String) throws -> Data {
guard let userED25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserED25519KeyPair() else {
internal static func encryptWithSessionProtocol(_ plaintext: Data, for recipientHexEncodedX25519PublicKey: String, using dependencies: Dependencies = Dependencies()) throws -> Data {
guard let userED25519KeyPair = dependencies.storage.getUserED25519KeyPair() else {
throw Error.noUserED25519KeyPair
}
let recipientX25519PublicKey = Data(hex: recipientHexEncodedX25519PublicKey.removingIdPrefixIfNeeded())
let sodium = Sodium()
let verificationData = plaintext + Data(userED25519KeyPair.publicKey) + recipientX25519PublicKey
guard let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) else {
guard let signature = dependencies.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) else {
throw Error.signingFailed
}
let plaintextWithMetadata = plaintext + Data(userED25519KeyPair.publicKey) + Data(signature)
guard let ciphertext = sodium.box.seal(message: Bytes(plaintextWithMetadata), recipientPublicKey: Bytes(recipientX25519PublicKey)) else {
guard let ciphertext = dependencies.box.seal(message: Bytes(plaintextWithMetadata), recipientPublicKey: Bytes(recipientX25519PublicKey)) else {
throw Error.encryptionFailed
}
@ -26,7 +24,7 @@ extension MessageSender {
internal static func encryptWithSessionBlindingProtocol(_ plaintext: Data, for recipientBlindedId: String, openGroupPublicKey: String, using dependencies: Dependencies = Dependencies()) throws -> Data {
guard SessionId.Prefix(from: recipientBlindedId) == .blinded else { throw Error.signingFailed }
guard let userEd25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserED25519KeyPair() else {
guard let userEd25519KeyPair = dependencies.storage.getUserED25519KeyPair() else {
throw Error.noUserED25519KeyPair
}
guard let blindedKeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: openGroupPublicKey, edKeyPair: userEd25519KeyPair, genericHash: dependencies.genericHash) else {

View File

@ -73,7 +73,7 @@ public enum ContactUtilities {
// Then we try loop through all approved contact threads to see if one of those contacts can be blinded to match
ContactUtilities.enumerateApprovedContactThreads(using: transaction) { contactThread, contact, stop in
guard dependencies.sodium.sessionId(contact.sessionID, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey) else {
guard dependencies.sodium.sessionId(contact.sessionID, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey, genericHash: dependencies.genericHash) else {
return
}
@ -91,7 +91,7 @@ public enum ContactUtilities {
// a thread with this contact in a different SOGS and had cached the mapping)
dependencies.storage.enumerateBlindedIdMapping(using: transaction) { mapping, stop in
guard mapping.serverPublicKey != serverPublicKey else { return }
guard dependencies.sodium.sessionId(mapping.sessionId, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey) else {
guard dependencies.sodium.sessionId(mapping.sessionId, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey, genericHash: dependencies.genericHash) else {
return
}

View File

@ -30,10 +30,16 @@ public class Dependencies {
set { _sodium = newValue }
}
internal var _aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType?
public var aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType {
get { Dependencies.getValueSettingIfNull(&_aeadXChaCha20Poly1305Ietf) { sodium.getAeadXChaCha20Poly1305Ietf() } }
set { _aeadXChaCha20Poly1305Ietf = newValue }
internal var _box: BoxType?
public var box: BoxType {
get { Dependencies.getValueSettingIfNull(&_box) { sodium.getBox() } }
set { _box = newValue }
}
internal var _genericHash: GenericHashType?
public var genericHash: GenericHashType {
get { Dependencies.getValueSettingIfNull(&_genericHash) { sodium.getGenericHash() } }
set { _genericHash = newValue }
}
internal var _sign: SignType?
@ -42,10 +48,10 @@ public class Dependencies {
set { _sign = newValue }
}
internal var _genericHash: GenericHashType?
public var genericHash: GenericHashType {
get { Dependencies.getValueSettingIfNull(&_genericHash) { sodium.getGenericHash() } }
set { _genericHash = newValue }
internal var _aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType?
public var aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType {
get { Dependencies.getValueSettingIfNull(&_aeadXChaCha20Poly1305Ietf) { sodium.getAeadXChaCha20Poly1305Ietf() } }
set { _aeadXChaCha20Poly1305Ietf = newValue }
}
internal var _ed25519: Ed25519Type?
@ -85,9 +91,10 @@ public class Dependencies {
identityManager: IdentityManagerProtocol? = nil,
storage: SessionMessagingKitStorageProtocol? = nil,
sodium: SodiumType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
sign: SignType? = nil,
box: BoxType? = nil,
genericHash: GenericHashType? = nil,
sign: SignType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
ed25519: Ed25519Type? = nil,
nonceGenerator16: NonceGenerator16ByteType? = nil,
nonceGenerator24: NonceGenerator24ByteType? = nil,
@ -98,9 +105,10 @@ public class Dependencies {
_identityManager = identityManager
_storage = storage
_sodium = sodium
_aeadXChaCha20Poly1305Ietf = aeadXChaCha20Poly1305Ietf
_sign = sign
_box = box
_genericHash = genericHash
_sign = sign
_aeadXChaCha20Poly1305Ietf = aeadXChaCha20Poly1305Ietf
_ed25519 = ed25519
_nonceGenerator16 = nonceGenerator16
_nonceGenerator24 = nonceGenerator24

View File

@ -41,6 +41,15 @@ extension Sign {
}
}
/// These extenion methods are used to generate a sign "blinded" messages
///
/// According to the Swift engineers the only situation when `UnsafeRawBufferPointer.baseAddress` is nil is when it's an
/// empty collection; as such our guard cases wihch return `-1` when unwrapping this value should never be hit and we can ignore
/// them as possible results.
///
/// For more information see:
/// https://forums.swift.org/t/when-is-unsafemutablebufferpointer-baseaddress-nil/32136/5
/// https://github.com/apple/swift-evolution/blob/master/proposals/0055-optional-unsafe-pointers.md#unsafebufferpointer
extension Sodium {
private static let scalarLength: Int = Int(crypto_core_ed25519_scalarbytes()) // 32
private static let noClampLength: Int = Int(crypto_scalarmult_ed25519_bytes()) // 32
@ -49,7 +58,7 @@ extension Sodium {
private static let secretKeyLength: Int = Int(crypto_sign_secretkeybytes()) // 64
/// 64-byte blake2b hash then reduce to get the blinding factor
public func generateBlindingFactor(serverPublicKey: String) -> Bytes? {
public func generateBlindingFactor(serverPublicKey: String, genericHash: GenericHashType) -> Bytes? {
/// k = salt.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
guard let serverPubKeyData: Data = serverPublicKey.dataFromHex() else { return nil }
guard let serverPublicKeyHashBytes: Bytes = genericHash.hash(message: [UInt8](serverPubKeyData), outputLength: 64) else {
@ -59,18 +68,15 @@ extension Sodium {
/// Reduce the server public key into an ed25519 scalar (`k`)
let kPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
let kResult = serverPublicKeyHashBytes.withUnsafeBytes { (serverPublicKeyHashPtr: UnsafeRawBufferPointer) -> Int32 in
_ = serverPublicKeyHashBytes.withUnsafeBytes { (serverPublicKeyHashPtr: UnsafeRawBufferPointer) -> Int32 in
guard let serverPublicKeyHashBaseAddress: UnsafePointer<UInt8> = serverPublicKeyHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
return -1 // Impossible case (refer to comments at top of extension)
}
crypto_core_ed25519_scalar_reduce(kPtr, serverPublicKeyHashBaseAddress)
return 0
}
/// Ensure the above worked
guard kResult == 0 else { return nil }
return Data(bytes: kPtr, count: Sodium.scalarLength).bytes
}
@ -78,21 +84,20 @@ extension Sodium {
/// convert to an *x* secret key, which seems wrong--but isn't because converted keys use the
/// same secret scalar secret (and so this is just the most convenient way to get 'a' out of
/// a sodium Ed25519 secret key)
private func generatePrivateKeyScalar(secretKey: Bytes) -> Bytes? {
func generatePrivateKeyScalar(secretKey: Bytes) -> Bytes {
/// a = s.to_curve25519_private_key().encode()
let aPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarMultLength)
let aResult = secretKey.withUnsafeBytes { (secretKeyPtr: UnsafeRawBufferPointer) -> Int32 in
/// Looks like the `crypto_sign_ed25519_sk_to_curve25519` function can't actually fail so no need to verify the result
/// See: https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_sign/ed25519/ref10/keypair.c#L70
_ = secretKey.withUnsafeBytes { (secretKeyPtr: UnsafeRawBufferPointer) -> Int32 in
guard let secretKeyBaseAddress: UnsafePointer<UInt8> = secretKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
return -1 // Impossible case (refer to comments at top of extension)
}
return crypto_sign_ed25519_sk_to_curve25519(aPtr, secretKeyBaseAddress)
}
/// Ensure the above worked
guard aResult == 0 else { return nil }
return Data(bytes: aPtr, count: Sodium.scalarMultLength).bytes
}
@ -101,20 +106,22 @@ extension Sodium {
guard edKeyPair.publicKey.count == Sodium.publicKeyLength && edKeyPair.secretKey.count == Sodium.secretKeyLength else {
return nil
}
guard let kBytes: Bytes = generateBlindingFactor(serverPublicKey: serverPublicKey) else { return nil }
guard let aBytes: Bytes = generatePrivateKeyScalar(secretKey: edKeyPair.secretKey) else { return nil }
guard let kBytes: Bytes = generateBlindingFactor(serverPublicKey: serverPublicKey, genericHash: genericHash) else {
return nil
}
let aBytes: Bytes = generatePrivateKeyScalar(secretKey: edKeyPair.secretKey)
/// Generate the blinded key pair `ka`, `kA`
let kaPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.secretKeyLength)
let kAPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.publicKeyLength)
let kaResult = aBytes.withUnsafeBytes { (aPtr: UnsafeRawBufferPointer) -> Int32 in
_ = aBytes.withUnsafeBytes { (aPtr: UnsafeRawBufferPointer) -> Int32 in
return kBytes.withUnsafeBytes { (kPtr: UnsafeRawBufferPointer) -> Int32 in
guard let kBaseAddress: UnsafePointer<UInt8> = kPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
return -1 // Impossible case (refer to comments at top of extension)
}
guard let aBaseAddress: UnsafePointer<UInt8> = aPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
return -1 // Impossible case (refer to comments at top of extension)
}
crypto_core_ed25519_scalar_mul(kaPtr, kBaseAddress, aBaseAddress)
@ -122,9 +129,6 @@ extension Sodium {
}
}
/// Ensure the above worked
guard kaResult == 0 else { return nil }
guard crypto_scalarmult_ed25519_base_noclamp(kAPtr, kaPtr) == 0 else { return nil }
return Box.KeyPair(
@ -144,18 +148,15 @@ extension Sodium {
let combinedHashBytes: Bytes = (H_rh + kA + message).sha512()
let rPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
let rResult = combinedHashBytes.withUnsafeBytes { (combinedHashPtr: UnsafeRawBufferPointer) -> Int32 in
_ = combinedHashBytes.withUnsafeBytes { (combinedHashPtr: UnsafeRawBufferPointer) -> Int32 in
guard let combinedHashBaseAddress: UnsafePointer<UInt8> = combinedHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
return -1 // Impossible case (refer to comments at top of extension)
}
crypto_core_ed25519_scalar_reduce(rPtr, combinedHashBaseAddress)
return 0
}
/// Ensure the above worked
guard rResult == 0 else { return nil }
/// sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r)
let sig_RPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.noClampLength)
guard crypto_scalarmult_ed25519_base_noclamp(sig_RPtr, rPtr) == 0 else { return nil }
@ -165,25 +166,22 @@ extension Sodium {
let HRAMHashBytes: Bytes = (sig_RBytes + kA + message).sha512()
let HRAMPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
let HRAMResult = HRAMHashBytes.withUnsafeBytes { (HRAMHashPtr: UnsafeRawBufferPointer) -> Int32 in
_ = HRAMHashBytes.withUnsafeBytes { (HRAMHashPtr: UnsafeRawBufferPointer) -> Int32 in
guard let HRAMHashBaseAddress: UnsafePointer<UInt8> = HRAMHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
return -1 // Impossible case (refer to comments at top of extension)
}
crypto_core_ed25519_scalar_reduce(HRAMPtr, HRAMHashBaseAddress)
return 0
}
/// Ensure the above worked
guard HRAMResult == 0 else { return nil }
/// sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka))
let sig_sMulPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
let sig_sPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
let sig_sResult = ka.withUnsafeBytes { (kaPtr: UnsafeRawBufferPointer) -> Int32 in
_ = ka.withUnsafeBytes { (kaPtr: UnsafeRawBufferPointer) -> Int32 in
guard let kaBaseAddress: UnsafePointer<UInt8> = kaPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
return -1 // Impossible case (refer to comments at top of extension)
}
crypto_core_ed25519_scalar_mul(sig_sMulPtr, HRAMPtr, kaBaseAddress)
@ -191,8 +189,6 @@ extension Sodium {
return 0
}
guard sig_sResult == 0 else { return nil }
/// full_sig = sig_R + sig_s
return (Data(bytes: sig_RPtr, count: Sodium.noClampLength).bytes + Data(bytes: sig_sPtr, count: Sodium.scalarLength).bytes)
}
@ -204,10 +200,10 @@ extension Sodium {
let result = rhsKeyBytes.withUnsafeBytes { (rhsKeyBytesPtr: UnsafeRawBufferPointer) -> Int32 in
return lhsKeyBytes.withUnsafeBytes { (lhsKeyBytesPtr: UnsafeRawBufferPointer) -> Int32 in
guard let lhsKeyBytesBaseAddress: UnsafePointer<UInt8> = lhsKeyBytesPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
return -1 // Impossible case (refer to comments at top of extension)
}
guard let rhsKeyBytesBaseAddress: UnsafePointer<UInt8> = rhsKeyBytesPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
return -1 // Impossible case (refer to comments at top of extension)
}
return crypto_scalarmult_ed25519_noclamp(combinedPtr, lhsKeyBytesBaseAddress, rhsKeyBytesBaseAddress)
@ -228,18 +224,23 @@ extension Sodium {
///
/// BLAKE2b(b kA || kA || kB)
public func sharedBlindedEncryptionKey(secretKey: Bytes, otherBlindedPublicKey: Bytes, fromBlindedPublicKey kA: Bytes, toBlindedPublicKey kB: Bytes, genericHash: GenericHashType) -> Bytes? {
guard let aBytes: Bytes = generatePrivateKeyScalar(secretKey: secretKey) else { return nil }
guard let combinedKeyBytes: Bytes = combineKeys(lhsKeyBytes: aBytes, rhsKeyBytes: otherBlindedPublicKey) else { return nil }
let aBytes: Bytes = generatePrivateKeyScalar(secretKey: secretKey)
guard let combinedKeyBytes: Bytes = combineKeys(lhsKeyBytes: aBytes, rhsKeyBytes: otherBlindedPublicKey) else {
return nil
}
return genericHash.hash(message: (combinedKeyBytes + kA + kB), outputLength: 32)
}
/// This method should be used to check if a users standard sessionId matches a blinded one
public func sessionId(_ standardSessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String) -> Bool {
public func sessionId(_ standardSessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String, genericHash: GenericHashType) -> Bool {
// Only support generating blinded keys for standard session ids
guard let sessionId: SessionId = SessionId(from: standardSessionId), sessionId.prefix == .standard else { return false }
guard let blindedId: SessionId = SessionId(from: blindedSessionId), blindedId.prefix == .blinded else { return false }
guard let kBytes: Bytes = generateBlindingFactor(serverPublicKey: serverPublicKey) else { return false }
guard let kBytes: Bytes = generateBlindingFactor(serverPublicKey: serverPublicKey, genericHash: genericHash) else {
return false
}
/// From the session id (ignoring 05 prefix) we have two possible ed25519 pubkeys; the first is the positive (which is what
/// Signal's XEd25519 conversion always uses)

View File

@ -0,0 +1,20 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Quick
import Nimble
@testable import SessionMessagingKit
class HeaderSpec: QuickSpec {
// MARK: - Spec
override func spec() {
describe("a Dictionary of Header to String values") {
it("can be converted into a dictionary of String to String values") {
expect([Header.authorization: "test"].toHTTPHeaders()).to(equal(["Authorization": "test"]))
}
}
}
}

View File

@ -0,0 +1,32 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Quick
import Nimble
@testable import SessionMessagingKit
class FileUploadResponseSpec: QuickSpec {
// MARK: - Spec
override func spec() {
describe("a FileUploadResponse") {
context("when decoding") {
it("handles a string id value") {
let jsonData: Data = "{\"id\":\"123\"}".data(using: .utf8)!
let response: FileUploadResponse? = try? JSONDecoder().decode(FileUploadResponse.self, from: jsonData)
expect(response?.id).to(equal("123"))
}
it("handles an int id value") {
let jsonData: Data = "{\"id\":124}".data(using: .utf8)!
let response: FileUploadResponse? = try? JSONDecoder().decode(FileUploadResponse.self, from: jsonData)
expect(response?.id).to(equal("124"))
}
}
}
}
}

View File

@ -0,0 +1,167 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Quick
import Nimble
@testable import SessionMessagingKit
class RequestSpec: QuickSpec {
struct TestType: Codable, Equatable {
let stringValue: String
}
// MARK: - Spec
override func spec() {
describe("a Request") {
it("is initialized with the correct default values") {
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
server: "testServer",
endpoint: .batch
)
expect(request.method.rawValue).to(equal("GET"))
expect(request.queryParameters).to(equal([:]))
expect(request.headers).to(equal([:]))
expect(request.body).to(beNil())
}
context("when generating a URL") {
it("adds a leading forward slash to the endpoint path") {
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
server: "testServer",
endpoint: .batch
)
expect(request.urlPathAndParamsString).to(equal("/batch"))
}
it("creates a valid URL with no query parameters") {
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
server: "testServer",
endpoint: .batch
)
expect(request.urlPathAndParamsString).to(equal("/batch"))
}
it("creates a valid URL when query parameters are provided") {
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
server: "testServer",
endpoint: .batch,
queryParameters: [
.limit: "123"
]
)
expect(request.urlPathAndParamsString).to(equal("/batch?limit=123"))
}
}
context("when generating a URLRequest") {
it("sets all the values correctly") {
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
method: .delete,
server: "testServer",
endpoint: .batch,
headers: [
.authorization: "test"
]
)
let urlRequest: URLRequest? = try? request.generateUrlRequest()
expect(urlRequest?.httpMethod).to(equal("DELETE"))
expect(urlRequest?.allHTTPHeaderFields).to(equal(["Authorization": "test"]))
expect(urlRequest?.httpBody).to(beNil())
}
it("throws an error if the URL is invalid") {
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
server: "testServer",
endpoint: .roomPollInfo("!!%%", 123)
)
expect {
try request.generateUrlRequest()
}
.to(throwError(HTTP.Error.invalidURL))
}
context("with a base64 string body") {
it("successfully encodes the body") {
let request: Request<String, OpenGroupAPI.Endpoint> = Request(
server: "testServer",
endpoint: .batch,
body: "TestMessage".data(using: .utf8)!.base64EncodedString()
)
let urlRequest: URLRequest? = try? request.generateUrlRequest()
let requestBody: Data? = Data(base64Encoded: urlRequest?.httpBody?.base64EncodedString() ?? "")
let requestBodyString: String? = String(data: requestBody ?? Data(), encoding: .utf8)
expect(requestBodyString).to(equal("TestMessage"))
}
it("throws an error if the body is not base64 encoded") {
let request: Request<String, OpenGroupAPI.Endpoint> = Request(
server: "testServer",
endpoint: .batch,
body: "TestMessage"
)
expect {
try request.generateUrlRequest()
}
.to(throwError(HTTP.Error.parsingFailed))
}
}
context("with a byte body") {
it("successfully encodes the body") {
let request: Request<[UInt8], OpenGroupAPI.Endpoint> = Request(
server: "testServer",
endpoint: .batch,
body: [1, 2, 3]
)
let urlRequest: URLRequest? = try? request.generateUrlRequest()
expect(urlRequest?.httpBody?.bytes).to(equal([1, 2, 3]))
}
}
context("with a JSON body") {
it("successfully encodes the body") {
let request: Request<TestType, OpenGroupAPI.Endpoint> = Request(
server: "testServer",
endpoint: .batch,
body: TestType(stringValue: "test")
)
let urlRequest: URLRequest? = try? request.generateUrlRequest()
let requestBody: TestType? = try? JSONDecoder().decode(
TestType.self,
from: urlRequest?.httpBody ?? Data()
)
expect(requestBody).to(equal(TestType(stringValue: "test")))
}
it("successfully encodes no body") {
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
server: "testServer",
endpoint: .batch,
body: nil
)
expect {
try request.generateUrlRequest()
}.toNot(throwError())
}
}
}
}
}
}

View File

@ -0,0 +1,48 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Quick
import Nimble
@testable import SessionMessagingKit
class BlindedIdMappingSpec: QuickSpec {
// MARK: - Spec
override func spec() {
describe("a BlindedIdMapping") {
context("when initializing") {
it("sets the values correctly") {
let mapping: BlindedIdMapping = BlindedIdMapping(
blindedId: "testBlindedId",
sessionId: "testSessionId",
serverPublicKey: "testPublicKey"
)
expect(mapping.blindedId).to(equal("testBlindedId"))
expect(mapping.sessionId).to(equal("testSessionId"))
expect(mapping.serverPublicKey).to(equal("testPublicKey"))
}
}
context("when NSCoding") {
// Note: Unit testing NSCoder is horrible so we won't do it properly - wait until we refactor it to Codable
it("successfully encodes and decodes") {
let mappingToEncode: BlindedIdMapping = BlindedIdMapping(
blindedId: "testBlindedId",
sessionId: "testSessionId",
serverPublicKey: "testPublicKey"
)
let encodedData: Data = try! NSKeyedArchiver.archivedData(withRootObject: mappingToEncode, requiringSecureCoding: false)
let mapping: BlindedIdMapping? = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(encodedData) as? BlindedIdMapping
expect(mapping).toNot(beNil())
expect(mapping?.blindedId).to(equal("testBlindedId"))
expect(mapping?.sessionId).to(equal("testSessionId"))
expect(mapping?.serverPublicKey).to(equal("testPublicKey"))
}
}
}
}
}

View File

@ -0,0 +1,385 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import SessionSnodeKit
import Quick
import Nimble
@testable import SessionMessagingKit
import AVFoundation
class BatchRequestInfoSpec: QuickSpec {
struct TestType: Codable, Equatable {
let stringValue: String
}
// MARK: - Spec
override func spec() {
// MARK: - BatchSubRequest
describe("a BatchSubRequest") {
var subRequest: OpenGroupAPI.BatchSubRequest!
context("when initializing") {
it("sets the headers to nil if there aren't any") {
subRequest = OpenGroupAPI.BatchSubRequest(
request: Request<NoBody, OpenGroupAPI.Endpoint>(
server: "testServer",
endpoint: .batch
)
)
expect(subRequest.headers).to(beNil())
}
it("converts the headers to HTTP headers") {
subRequest = OpenGroupAPI.BatchSubRequest(
request: Request<NoBody, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [.authorization: "testAuth"],
body: nil
)
)
expect(subRequest.headers).to(equal(["Authorization": "testAuth"]))
}
}
context("when encoding") {
it("successfully encodes a string body") {
subRequest = OpenGroupAPI.BatchSubRequest(
request: Request<String, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [:],
body: "testBody"
)
)
let subRequestData: Data = try! JSONEncoder().encode(subRequest)
let subRequestString: String? = String(data: subRequestData, encoding: .utf8)
expect(subRequestString)
.to(equal("{\"path\":\"\\/batch\",\"method\":\"GET\",\"b64\":\"testBody\"}"))
}
it("successfully encodes a byte body") {
subRequest = OpenGroupAPI.BatchSubRequest(
request: Request<[UInt8], OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [:],
body: [1, 2, 3]
)
)
let subRequestData: Data = try! JSONEncoder().encode(subRequest)
let subRequestString: String? = String(data: subRequestData, encoding: .utf8)
expect(subRequestString)
.to(equal("{\"path\":\"\\/batch\",\"method\":\"GET\",\"bytes\":[1,2,3]}"))
}
it("successfully encodes a JSON body") {
subRequest = OpenGroupAPI.BatchSubRequest(
request: Request<TestType, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [:],
body: TestType(stringValue: "testValue")
)
)
let subRequestData: Data = try! JSONEncoder().encode(subRequest)
let subRequestString: String? = String(data: subRequestData, encoding: .utf8)
expect(subRequestString)
.to(equal("{\"path\":\"\\/batch\",\"method\":\"GET\",\"json\":{\"stringValue\":\"testValue\"}}"))
}
}
}
// MARK: - BatchSubResponse<T>
describe("a BatchSubResponse<T>") {
context("when decoding") {
it("decodes correctly") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
"body": {
"stringValue": "testValue"
}
}
"""
let subResponse: OpenGroupAPI.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
OpenGroupAPI.BatchSubResponse<TestType>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
expect(subResponse?.body).toNot(beNil())
}
it("decodes with invalid body data") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
"body": "Hello!!!"
}
"""
let subResponse: OpenGroupAPI.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
OpenGroupAPI.BatchSubResponse<TestType>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
}
it("flags invalid body data as invalid") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
"body": "Hello!!!"
}
"""
let subResponse: OpenGroupAPI.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
OpenGroupAPI.BatchSubResponse<TestType>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
expect(subResponse?.body).to(beNil())
expect(subResponse?.failedToParseBody).to(beTrue())
}
it("does not flag a missing or invalid optional body as invalid") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
}
"""
let subResponse: OpenGroupAPI.BatchSubResponse<TestType?>? = try? JSONDecoder().decode(
OpenGroupAPI.BatchSubResponse<TestType?>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
expect(subResponse?.body).to(beNil())
expect(subResponse?.failedToParseBody).to(beFalse())
}
it("does not flag a NoResponse body as invalid") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
}
"""
let subResponse: OpenGroupAPI.BatchSubResponse<NoResponse>? = try? JSONDecoder().decode(
OpenGroupAPI.BatchSubResponse<NoResponse>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
expect(subResponse?.body).to(beNil())
expect(subResponse?.failedToParseBody).to(beFalse())
}
}
}
// MARK: - BatchRequestInfo<T, R>
describe("a BatchRequestInfo<T, R>") {
var request: Request<TestType, OpenGroupAPI.Endpoint>!
beforeEach {
request = Request(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [:],
body: TestType(stringValue: "testValue")
)
}
it("initializes correctly when given a request") {
let requestInfo: OpenGroupAPI.BatchRequestInfo<TestType> = OpenGroupAPI.BatchRequestInfo(
request: request
)
expect(requestInfo.request).to(equal(request))
expect(requestInfo.responseType == OpenGroupAPI.BatchSubResponse<NoResponse>.self).to(beTrue())
}
it("initializes correctly when given a request and a response type") {
let requestInfo: OpenGroupAPI.BatchRequestInfo<TestType> = OpenGroupAPI.BatchRequestInfo(
request: request,
responseType: TestType.self
)
expect(requestInfo.request).to(equal(request))
expect(requestInfo.responseType == OpenGroupAPI.BatchSubResponse<TestType>.self).to(beTrue())
}
it("exposes the endpoint correctly") {
let requestInfo: OpenGroupAPI.BatchRequestInfo<TestType> = OpenGroupAPI.BatchRequestInfo(
request: request
)
expect(requestInfo.endpoint.path).to(equal(request.endpoint.path))
}
it("generates a sub request correctly") {
let requestInfo: OpenGroupAPI.BatchRequestInfo<TestType> = OpenGroupAPI.BatchRequestInfo(
request: request
)
let subRequest: OpenGroupAPI.BatchSubRequest = requestInfo.toSubRequest()
expect(subRequest.method).to(equal(request.method))
expect(subRequest.path).to(equal(request.urlPathAndParamsString))
expect(subRequest.headers).to(beNil())
}
}
// MARK: - Convenience
// MARK: --Decodable
describe("a Decodable") {
it("decodes correctly") {
let jsonData: Data = "{\"stringValue\":\"testValue\"}".data(using: .utf8)!
let result: TestType? = try? TestType.decoded(from: jsonData)
expect(result).to(equal(TestType(stringValue: "testValue")))
}
}
// MARK: - --Promise
describe("an (OnionRequestResponseInfoType, Data?) Promise") {
var responseInfo: OnionRequestResponseInfoType!
var capabilities: OpenGroupAPI.Capabilities!
var pinnedMessage: OpenGroupAPI.PinnedMessage!
var data: Data!
beforeEach {
responseInfo = OnionRequestAPI.ResponseInfo(code: 200, headers: [:])
capabilities = OpenGroupAPI.Capabilities(capabilities: [], missing: nil)
pinnedMessage = OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 123, pinnedBy: "test")
data = """
[\([
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
code: 200,
headers: [:],
body: capabilities,
failedToParseBody: false
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
code: 200,
headers: [:],
body: pinnedMessage,
failedToParseBody: false
)
)
]
.map { String(data: $0, encoding: .utf8)! }
.joined(separator: ","))]
""".data(using: .utf8)!
}
it("decodes valid data correctly") {
let result = Promise.value((responseInfo, data))
.decoded(as: [
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>.self,
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>.self
])
expect(result.value).toNot(beNil())
expect((result.value?[0].1 as? OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>)?.body)
.to(equal(capabilities))
expect((result.value?[1].1 as? OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>)?.body)
.to(equal(pinnedMessage))
}
it("fails if there is no data") {
let result = Promise.value((responseInfo, nil)).decoded(as: [])
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
}
it("fails if the data is not JSON") {
let result = Promise.value((responseInfo, Data([1, 2, 3]))).decoded(as: [])
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
}
it("fails if the data is not a JSON array") {
let result = Promise.value((responseInfo, "{}".data(using: .utf8))).decoded(as: [])
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
}
it("fails if the JSON array does not have the same number of items as the expected types") {
let result = Promise.value((responseInfo, data))
.decoded(as: [
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>.self,
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>.self,
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>.self
])
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
}
it("fails if one of the JSON array values fails to decode") {
data = """
[\([
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
code: 200,
headers: [:],
body: capabilities,
failedToParseBody: false
)
)
]
.map { String(data: $0, encoding: .utf8)! }
.joined(separator: ",")),{"test": "test"}]
""".data(using: .utf8)!
let result = Promise.value((responseInfo, data))
.decoded(as: [
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>.self,
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>.self
])
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
}
}
}
}

View File

@ -29,7 +29,7 @@ class OpenGroupSpec: QuickSpec {
}
context("when NSCoding") {
// Note: Unit testing NSCoder is horrible so we won't do it - wait until we refactor it to Codable
// Note: Unit testing NSCoder is horrible so we won't do it properly - wait until we refactor it to Codable
it("successfully encodes and decodes") {
let openGroupToEncode: OpenGroup = OpenGroup(
server: "server",

View File

@ -24,7 +24,7 @@ class ServerSpec: QuickSpec {
}
context("when NSCoding") {
// Note: Unit testing NSCoder is horrible so we won't do it - wait until we refactor it to Codable
// Note: Unit testing NSCoder is horrible so we won't do it properly - wait until we refactor it to Codable
it("successfully encodes and decodes") {
let serverToEncode: OpenGroupAPI.Server = OpenGroupAPI.Server(
name: "test",

View File

@ -43,9 +43,9 @@ class OpenGroupAPISpec: QuickSpec {
onionApi: TestOnionRequestAPI.self,
storage: mockStorage,
sodium: mockSodium,
aeadXChaCha20Poly1305Ietf: mockAeadXChaCha20Poly1305Ietf,
sign: mockSign,
genericHash: mockGenericHash,
sign: mockSign,
aeadXChaCha20Poly1305Ietf: mockAeadXChaCha20Poly1305Ietf,
ed25519: mockEd25519,
nonceGenerator16: mockNonce16Generator,
nonceGenerator24: mockNonce24Generator,
@ -229,7 +229,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(requestData?.urlString).to(equal("testServer/batch"))
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.server).to(equal("testServer"))
expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"))
expect(requestData?.publicKey).to(equal("88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
}
it("retrieves recent messages if there was no last message") {
@ -2663,10 +2663,10 @@ class OpenGroupAPISpec: QuickSpec {
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?.publicKey).to(equal("88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
expect(requestData?.headers).to(haveCount(4))
expect(requestData?.headers[Header.sogsPubKey.rawValue])
.to(equal("007aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"))
.to(equal("0088672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
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("TestSignature".bytes.toBase64()))
@ -2720,9 +2720,9 @@ class OpenGroupAPISpec: QuickSpec {
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?.publicKey).to(equal("88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
expect(requestData?.headers).to(haveCount(4))
expect(requestData?.headers[Header.sogsPubKey.rawValue]).to(equal("157aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"))
expect(requestData?.headers[Header.sogsPubKey.rawValue]).to(equal("1588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
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()))

View File

@ -114,9 +114,9 @@ class OpenGroupManagerSpec: QuickSpec {
identityManager: mockIdentityManager,
storage: mockStorage,
sodium: mockSodium,
aeadXChaCha20Poly1305Ietf: mockAeadXChaCha20Poly1305Ietf,
sign: mockSign,
genericHash: mockGenericHash,
sign: mockSign,
aeadXChaCha20Poly1305Ietf: mockAeadXChaCha20Poly1305Ietf,
ed25519: MockEd25519(),
nonceGenerator16: mockNonce16Generator,
nonceGenerator24: mockNonce24Generator,
@ -1600,7 +1600,7 @@ class OpenGroupManagerSpec: QuickSpec {
expect(didComplete).toEventually(beTrue(), timeout: .milliseconds(50))
expect(mockStorage)
.to(call(matchingParameters: true) {
.toEventually(call(matchingParameters: true) {
$0.setOpenGroup(
OpenGroup(
server: "testServer",
@ -1620,6 +1620,11 @@ class OpenGroupManagerSpec: QuickSpec {
beNil(),
timeout: .milliseconds(50)
)
expect(mockOGMCache)
.toEventually(
call(.exactly(times: 1)) { $0.groupImagePromises },
timeout: .milliseconds(50)
)
expect(testGroupThread.numSaveCalls)
.toEventually(
equal(2), // Call to save the open group and then to save the image
@ -2121,7 +2126,7 @@ class OpenGroupManagerSpec: QuickSpec {
}
.thenReturn([])
mockSodium
.when { $0.generateBlindingFactor(serverPublicKey: any()) }
.when { $0.generateBlindingFactor(serverPublicKey: any(), genericHash: mockGenericHash) }
.thenReturn([])
mockAeadXChaCha20Poly1305Ietf
.when {

View File

@ -0,0 +1,500 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import Quick
import Nimble
@testable import SessionMessagingKit
class MessageReceiverDecryptionSpec: QuickSpec {
// MARK: - Spec
override func spec() {
var mockStorage: MockStorage!
var mockSodium: MockSodium!
var mockBox: MockBox!
var mockGenericHash: MockGenericHash!
var mockSign: MockSign!
var mockAeadXChaCha: MockAeadXChaCha20Poly1305Ietf!
var mockNonce24Generator: MockNonce24Generator!
var dependencies: Dependencies!
describe("a MessageReceiver") {
beforeEach {
mockStorage = MockStorage()
mockSodium = MockSodium()
mockBox = MockBox()
mockGenericHash = MockGenericHash()
mockSign = MockSign()
mockAeadXChaCha = MockAeadXChaCha20Poly1305Ietf()
mockNonce24Generator = MockNonce24Generator()
mockAeadXChaCha
.when { $0.encrypt(message: anyArray(), secretKey: anyArray(), nonce: anyArray()) }
.thenReturn(nil)
dependencies = Dependencies(
storage: mockStorage,
sodium: mockSodium,
box: mockBox,
genericHash: mockGenericHash,
sign: mockSign,
aeadXChaCha20Poly1305Ietf: mockAeadXChaCha,
nonceGenerator24: mockNonce24Generator
)
mockStorage
.when { $0.getUserED25519KeyPair() }
.thenReturn(
Box.KeyPair(
publicKey: Data(hex: TestConstants.edPublicKey).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
)
)
mockBox
.when {
$0.open(
anonymousCipherText: anyArray(),
recipientPublicKey: anyArray(),
recipientSecretKey: anyArray()
)
}
.thenReturn([UInt8](repeating: 0, count: 100))
mockSodium
.when { $0.blindedKeyPair(serverPublicKey: any(), edKeyPair: any(), genericHash: mockGenericHash) }
.thenReturn(
Box.KeyPair(
publicKey: Data(hex: TestConstants.blindedPublicKey).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
)
)
mockSodium
.when {
$0.sharedBlindedEncryptionKey(
secretKey: anyArray(),
otherBlindedPublicKey: anyArray(),
fromBlindedPublicKey: anyArray(),
toBlindedPublicKey: anyArray(),
genericHash: mockGenericHash
)
}
.thenReturn([])
mockSodium
.when { $0.generateBlindingFactor(serverPublicKey: any(), genericHash: mockGenericHash) }
.thenReturn([])
mockSodium
.when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) }
.thenReturn(Data(hex: TestConstants.blindedPublicKey).bytes)
mockSign
.when { $0.toX25519(ed25519PublicKey: anyArray()) }
.thenReturn(Data(hex: TestConstants.publicKey).bytes)
mockSign
.when { $0.verify(message: anyArray(), publicKey: anyArray(), signature: anyArray()) }
.thenReturn(true)
mockAeadXChaCha
.when { $0.decrypt(authenticatedCipherText: anyArray(), secretKey: anyArray(), nonce: anyArray()) }
.thenReturn("TestMessage".data(using: .utf8)!.bytes + [UInt8](repeating: 0, count: 32))
mockNonce24Generator
.when { $0.nonce() }
.thenReturn(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes)
}
context("when decrypting with the session protocol") {
it("successfully decrypts a message") {
let result = try? MessageReceiver.decryptWithSessionProtocol(
ciphertext: Data(
base64Encoded: "SRP0eBUWh4ez6ppWjUs5/Wph5fhnPRgB5zsWWnTz+FBAw/YI3oS2pDpIfyetMTbU" +
"sFMhE5G4PbRtQFey1hsxLl221Qivc3ayaX2Mm/X89Dl8e45BC+Lb/KU9EdesxIK4pVgYXs9XrMtX3v8" +
"dt0eBaXneOBfr7qB8pHwwMZjtkOu1ED07T9nszgbWabBphUfWXe2U9K3PTRisSCI="
)!,
using: try! ECKeyPair(
publicKeyData: Data.data(fromHex: TestConstants.publicKey)!,
privateKeyData: Data.data(fromHex: TestConstants.privateKey)!
),
dependencies: Dependencies()
)
expect(String(data: (result?.plaintext ?? Data()), encoding: .utf8)).to(equal("TestMessage"))
expect(result?.senderX25519PublicKey)
.to(equal("0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
}
it("throws an error if it cannot open the message") {
mockBox
.when {
$0.open(
anonymousCipherText: anyArray(),
recipientPublicKey: anyArray(),
recipientSecretKey: anyArray()
)
}
.thenReturn(nil)
expect {
try MessageReceiver.decryptWithSessionProtocol(
ciphertext: "TestMessage".data(using: .utf8)!,
using: try! ECKeyPair(
publicKeyData: Data.data(fromHex: TestConstants.publicKey)!,
privateKeyData: Data.data(fromHex: TestConstants.privateKey)!
),
dependencies: dependencies
)
}
.to(throwError(MessageReceiver.Error.decryptionFailed))
}
it("throws an error if the open message is too short") {
mockBox
.when {
$0.open(
anonymousCipherText: anyArray(),
recipientPublicKey: anyArray(),
recipientSecretKey: anyArray()
)
}
.thenReturn([1, 2, 3])
expect {
try MessageReceiver.decryptWithSessionProtocol(
ciphertext: "TestMessage".data(using: .utf8)!,
using: try! ECKeyPair(
publicKeyData: Data.data(fromHex: TestConstants.publicKey)!,
privateKeyData: Data.data(fromHex: TestConstants.privateKey)!
),
dependencies: dependencies
)
}
.to(throwError(MessageReceiver.Error.decryptionFailed))
}
it("throws an error if it cannot verify the message") {
mockSign
.when { $0.verify(message: anyArray(), publicKey: anyArray(), signature: anyArray()) }
.thenReturn(false)
expect {
try MessageReceiver.decryptWithSessionProtocol(
ciphertext: "TestMessage".data(using: .utf8)!,
using: try! ECKeyPair(
publicKeyData: Data.data(fromHex: TestConstants.publicKey)!,
privateKeyData: Data.data(fromHex: TestConstants.privateKey)!
),
dependencies: dependencies
)
}
.to(throwError(MessageReceiver.Error.invalidSignature))
}
it("throws an error if it cannot get the senders x25519 public key") {
mockSign.when { $0.toX25519(ed25519PublicKey: anyArray()) }.thenReturn(nil)
expect {
try MessageReceiver.decryptWithSessionProtocol(
ciphertext: "TestMessage".data(using: .utf8)!,
using: try! ECKeyPair(
publicKeyData: Data.data(fromHex: TestConstants.publicKey)!,
privateKeyData: Data.data(fromHex: TestConstants.privateKey)!
),
dependencies: dependencies
)
}
.to(throwError(MessageReceiver.Error.decryptionFailed))
}
}
context("when decrypting with the blinded session protocol") {
it("successfully decrypts a message") {
let result = try? MessageReceiver.decryptWithSessionBlindingProtocol(
data: Data(
hex: "00db16b6687382811d69875a5376f66acad9c49fe5e26bcf770c7e6e9c230299" +
"f61b315299dd1fa700dd7f34305c0465af9e64dc791d7f4123f1eeafa5b4d48b3ade4" +
"f4b2a2764762e5a2c7900f254bd91633b43"
),
isOutgoing: true,
otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)",
with: TestConstants.serverPublicKey,
userEd25519KeyPair: Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
),
using: Dependencies()
)
expect(String(data: (result?.plaintext ?? Data()), encoding: .utf8)).to(equal("TestMessage"))
expect(result?.senderX25519PublicKey)
.to(equal("0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
}
it("successfully decrypts a mocked incoming message") {
let result = try? MessageReceiver.decryptWithSessionBlindingProtocol(
data: (
Data([0]) +
"TestMessage".data(using: .utf8)! +
Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!
),
isOutgoing: false,
otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)",
with: TestConstants.serverPublicKey,
userEd25519KeyPair: Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
),
using: dependencies
)
expect(String(data: (result?.plaintext ?? Data()), encoding: .utf8)).to(equal("TestMessage"))
expect(result?.senderX25519PublicKey)
.to(equal("0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
}
it("throws an error if the data is too short") {
expect {
try MessageReceiver.decryptWithSessionBlindingProtocol(
data: Data([1, 2, 3]),
isOutgoing: true,
otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)",
with: TestConstants.serverPublicKey,
userEd25519KeyPair: Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
),
using: dependencies
)
}
.to(throwError(MessageReceiver.Error.decryptionFailed))
}
it("throws an error if it cannot get the blinded keyPair") {
mockSodium
.when { $0.blindedKeyPair(serverPublicKey: any(), edKeyPair: any(), genericHash: mockGenericHash) }
.thenReturn(nil)
expect {
try MessageReceiver.decryptWithSessionBlindingProtocol(
data: (
Data([0]) +
"TestMessage".data(using: .utf8)! +
Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!
),
isOutgoing: true,
otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)",
with: TestConstants.serverPublicKey,
userEd25519KeyPair: Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
),
using: dependencies
)
}
.to(throwError(MessageReceiver.Error.decryptionFailed))
}
it("throws an error if it cannot get the decryption key") {
mockSodium
.when {
$0.sharedBlindedEncryptionKey(
secretKey: anyArray(),
otherBlindedPublicKey: anyArray(),
fromBlindedPublicKey: anyArray(),
toBlindedPublicKey: anyArray(),
genericHash: mockGenericHash
)
}
.thenReturn(nil)
expect {
try MessageReceiver.decryptWithSessionBlindingProtocol(
data: (
Data([0]) +
"TestMessage".data(using: .utf8)! +
Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!
),
isOutgoing: true,
otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)",
with: TestConstants.serverPublicKey,
userEd25519KeyPair: Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
),
using: dependencies
)
}
.to(throwError(MessageReceiver.Error.decryptionFailed))
}
it("throws an error if the data version is not 0") {
expect {
try MessageReceiver.decryptWithSessionBlindingProtocol(
data: (
Data([1]) +
"TestMessage".data(using: .utf8)! +
Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!
),
isOutgoing: true,
otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)",
with: TestConstants.serverPublicKey,
userEd25519KeyPair: Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
),
using: dependencies
)
}
.to(throwError(MessageReceiver.Error.decryptionFailed))
}
it("throws an error if it cannot decrypt the data") {
mockAeadXChaCha
.when { $0.decrypt(authenticatedCipherText: anyArray(), secretKey: anyArray(), nonce: anyArray()) }
.thenReturn(nil)
expect {
try MessageReceiver.decryptWithSessionBlindingProtocol(
data: (
Data([0]) +
"TestMessage".data(using: .utf8)! +
Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!
),
isOutgoing: true,
otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)",
with: TestConstants.serverPublicKey,
userEd25519KeyPair: Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
),
using: dependencies
)
}
.to(throwError(MessageReceiver.Error.decryptionFailed))
}
it("throws an error if the inner bytes are too short") {
mockAeadXChaCha
.when { $0.decrypt(authenticatedCipherText: anyArray(), secretKey: anyArray(), nonce: anyArray()) }
.thenReturn([1, 2, 3])
expect {
try MessageReceiver.decryptWithSessionBlindingProtocol(
data: (
Data([0]) +
"TestMessage".data(using: .utf8)! +
Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!
),
isOutgoing: true,
otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)",
with: TestConstants.serverPublicKey,
userEd25519KeyPair: Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
),
using: dependencies
)
}
.to(throwError(MessageReceiver.Error.decryptionFailed))
}
it("throws an error if it cannot generate the blinding factor") {
mockSodium
.when { $0.generateBlindingFactor(serverPublicKey: any(), genericHash: mockGenericHash) }
.thenReturn(nil)
expect {
try MessageReceiver.decryptWithSessionBlindingProtocol(
data: (
Data([0]) +
"TestMessage".data(using: .utf8)! +
Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!
),
isOutgoing: true,
otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)",
with: TestConstants.serverPublicKey,
userEd25519KeyPair: Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
),
using: dependencies
)
}
.to(throwError(MessageReceiver.Error.invalidSignature))
}
it("throws an error if it cannot generate the combined key") {
mockSodium
.when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) }
.thenReturn(nil)
expect {
try MessageReceiver.decryptWithSessionBlindingProtocol(
data: (
Data([0]) +
"TestMessage".data(using: .utf8)! +
Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!
),
isOutgoing: true,
otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)",
with: TestConstants.serverPublicKey,
userEd25519KeyPair: Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
),
using: dependencies
)
}
.to(throwError(MessageReceiver.Error.invalidSignature))
}
it("throws an error if the combined key does not match kA") {
mockSodium
.when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) }
.thenReturn(Data(hex: TestConstants.publicKey).bytes)
expect {
try MessageReceiver.decryptWithSessionBlindingProtocol(
data: (
Data([0]) +
"TestMessage".data(using: .utf8)! +
Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!
),
isOutgoing: true,
otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)",
with: TestConstants.serverPublicKey,
userEd25519KeyPair: Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
),
using: dependencies
)
}
.to(throwError(MessageReceiver.Error.invalidSignature))
}
it("throws an error if it cannot get the senders x25519 public key") {
mockSign
.when { $0.toX25519(ed25519PublicKey: anyArray()) }
.thenReturn(nil)
expect {
try MessageReceiver.decryptWithSessionBlindingProtocol(
data: (
Data([0]) +
"TestMessage".data(using: .utf8)! +
Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!
),
isOutgoing: true,
otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)",
with: TestConstants.serverPublicKey,
userEd25519KeyPair: Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
),
using: dependencies
)
}
.to(throwError(MessageReceiver.Error.decryptionFailed))
}
}
}
}
}

View File

@ -0,0 +1,272 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import Quick
import Nimble
@testable import SessionMessagingKit
class MessageSenderEncryptionSpec: QuickSpec {
// MARK: - Spec
override func spec() {
var mockStorage: MockStorage!
var mockBox: MockBox!
var mockSign: MockSign!
var mockNonce24Generator: MockNonce24Generator!
var dependencies: Dependencies!
describe("a MessageSender") {
beforeEach {
mockStorage = MockStorage()
mockBox = MockBox()
mockSign = MockSign()
mockNonce24Generator = MockNonce24Generator()
dependencies = Dependencies(
storage: mockStorage,
box: mockBox,
sign: mockSign,
nonceGenerator24: mockNonce24Generator
)
mockStorage.when { $0.getUserED25519KeyPair() }
.thenReturn(
Box.KeyPair(
publicKey: Data(hex: TestConstants.edPublicKey).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
)
)
mockNonce24Generator
.when { $0.nonce() }
.thenReturn(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes)
}
context("when encrypting with the session protocol") {
beforeEach {
mockBox.when { $0.seal(message: anyArray(), recipientPublicKey: anyArray()) }.thenReturn([1, 2, 3])
mockSign.when { $0.signature(message: anyArray(), secretKey: anyArray()) }.thenReturn([])
}
it("can encrypt correctly") {
let result = try? MessageSender.encryptWithSessionProtocol(
"TestMessage".data(using: .utf8)!,
for: "05\(TestConstants.publicKey)",
using: Dependencies(storage: mockStorage)
)
// Note: A Nonce is used for this so we can't compare the exact value when not mocked
expect(result).toNot(beNil())
expect(result?.count).to(equal(155))
}
it("returns the correct value when mocked") {
let result = try? MessageSender.encryptWithSessionProtocol(
"TestMessage".data(using: .utf8)!,
for: "05\(TestConstants.publicKey)",
using: dependencies
)
expect(result?.bytes).to(equal([1, 2, 3]))
}
it("throws an error if there is no ed25519 keyPair") {
mockStorage.when { $0.getUserED25519KeyPair() }.thenReturn(nil)
expect {
try MessageSender.encryptWithSessionProtocol(
"TestMessage".data(using: .utf8)!,
for: "05\(TestConstants.publicKey)",
using: dependencies
)
}
.to(throwError(MessageSender.Error.noUserED25519KeyPair))
}
it("throws an error if the signature generation fails") {
mockSign.when { $0.signature(message: anyArray(), secretKey: anyArray()) }.thenReturn(nil)
expect {
try MessageSender.encryptWithSessionProtocol(
"TestMessage".data(using: .utf8)!,
for: "05\(TestConstants.publicKey)",
using: dependencies
)
}
.to(throwError(MessageSender.Error.signingFailed))
}
it("throws an error if the encryption fails") {
mockBox.when { $0.seal(message: anyArray(), recipientPublicKey: anyArray()) }.thenReturn(nil)
expect {
try MessageSender.encryptWithSessionProtocol(
"TestMessage".data(using: .utf8)!,
for: "05\(TestConstants.publicKey)",
using: dependencies
)
}
.to(throwError(MessageSender.Error.encryptionFailed))
}
}
context("when encrypting with the blinded session protocol") {
it("successfully encrypts") {
let result = try? MessageSender.encryptWithSessionBlindingProtocol(
"TestMessage".data(using: .utf8)!,
for: "15\(TestConstants.blindedPublicKey)",
openGroupPublicKey: TestConstants.serverPublicKey,
using: dependencies
)
expect(result?.toHexString())
.to(equal(
"00db16b6687382811d69875a5376f66acad9c49fe5e26bcf770c7e6e9c230299" +
"f61b315299dd1fa700dd7f34305c0465af9e64dc791d7f4123f1eeafa5b4d48b" +
"3ade4f4b2a2764762e5a2c7900f254bd91633b43"
))
}
it("includes a version at the start of the encrypted value") {
let result = try? MessageSender.encryptWithSessionBlindingProtocol(
"TestMessage".data(using: .utf8)!,
for: "15\(TestConstants.blindedPublicKey)",
openGroupPublicKey: TestConstants.serverPublicKey,
using: dependencies
)
expect(result?.toHexString().prefix(2)).to(equal("00"))
}
it("includes the nonce at the end of the encrypted value") {
let maybeResult = try? MessageSender.encryptWithSessionBlindingProtocol(
"TestMessage".data(using: .utf8)!,
for: "15\(TestConstants.blindedPublicKey)",
openGroupPublicKey: TestConstants.serverPublicKey,
using: dependencies
)
let result: [UInt8] = (maybeResult?.bytes ?? [])
let nonceBytes: [UInt8] = Array(result[max(0, (result.count - 24))..<result.count])
expect(Data(nonceBytes).base64EncodedString())
.to(equal("pbTUizreT0sqJ2R2LloseQDyVL2RYztD"))
}
it("throws an error if the recipient isn't a blinded id") {
expect {
try MessageSender.encryptWithSessionBlindingProtocol(
"TestMessage".data(using: .utf8)!,
for: "05\(TestConstants.publicKey)",
openGroupPublicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
.to(throwError(MessageSender.Error.signingFailed))
}
it("throws an error if there is no ed25519 keyPair") {
mockStorage.when { $0.getUserED25519KeyPair() }.thenReturn(nil)
expect {
try MessageSender.encryptWithSessionBlindingProtocol(
"TestMessage".data(using: .utf8)!,
for: "15\(TestConstants.blindedPublicKey)",
openGroupPublicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
.to(throwError(MessageSender.Error.noUserED25519KeyPair))
}
it("throws an error if it fails to generate a blinded keyPair") {
let mockSodium: MockSodium = MockSodium()
let mockGenericHash: MockGenericHash = MockGenericHash()
dependencies = dependencies.with(sodium: mockSodium, genericHash: mockGenericHash)
mockSodium
.when {
$0.blindedKeyPair(
serverPublicKey: any(),
edKeyPair: any(),
genericHash: mockGenericHash
)
}
.thenReturn(nil)
expect {
try MessageSender.encryptWithSessionBlindingProtocol(
"TestMessage".data(using: .utf8)!,
for: "15\(TestConstants.blindedPublicKey)",
openGroupPublicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
.to(throwError(MessageSender.Error.signingFailed))
}
it("throws an error if it fails to generate an encryption key") {
let mockSodium: MockSodium = MockSodium()
let mockGenericHash: MockGenericHash = MockGenericHash()
dependencies = dependencies.with(sodium: mockSodium, genericHash: mockGenericHash)
mockSodium
.when {
$0.blindedKeyPair(
serverPublicKey: any(),
edKeyPair: any(),
genericHash: mockGenericHash
)
}
.thenReturn(
Box.KeyPair(
publicKey: Data(hex: TestConstants.edPublicKey).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
)
)
mockSodium
.when {
$0.sharedBlindedEncryptionKey(
secretKey: anyArray(),
otherBlindedPublicKey: anyArray(),
fromBlindedPublicKey: anyArray(),
toBlindedPublicKey: anyArray(),
genericHash: mockGenericHash
)
}
.thenReturn(nil)
expect {
try MessageSender.encryptWithSessionBlindingProtocol(
"TestMessage".data(using: .utf8)!,
for: "15\(TestConstants.blindedPublicKey)",
openGroupPublicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
.to(throwError(MessageSender.Error.signingFailed))
}
it("throws an error if it fails to encrypt") {
let mockAeadXChaCha: MockAeadXChaCha20Poly1305Ietf = MockAeadXChaCha20Poly1305Ietf()
dependencies = dependencies.with(aeadXChaCha20Poly1305Ietf: mockAeadXChaCha)
mockAeadXChaCha
.when { $0.encrypt(message: anyArray(), secretKey: anyArray(), nonce: anyArray()) }
.thenReturn(nil)
expect {
try MessageSender.encryptWithSessionBlindingProtocol(
"TestMessage".data(using: .utf8)!,
for: "15\(TestConstants.blindedPublicKey)",
openGroupPublicKey: TestConstants.serverPublicKey,
using: dependencies
)
}
.to(throwError(MessageSender.Error.encryptionFailed))
}
}
}
}
}

View File

@ -0,0 +1,351 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import Quick
import Nimble
@testable import SessionMessagingKit
class SodiumUtilitiesSpec: QuickSpec {
// MARK: - Spec
override func spec() {
// MARK: - Sign
describe("an extended Sign") {
var sign: Sign!
beforeEach {
sign = Sodium().sign
}
it("can convert an ed25519 public key into an x25519 public key") {
let result = sign.toX25519(ed25519PublicKey: TestConstants.edPublicKey.bytes)
expect(result?.toHexString())
.to(equal("95ffb559d4e804e9b414a5178454c426f616b4a61089b217b41165dbb7c9fe2d"))
}
it("can convert an ed25519 private key into an x25519 private key") {
let result = sign.toX25519(ed25519SecretKey: TestConstants.edSecretKey.bytes)
expect(result?.toHexString())
.to(equal("c83f9a1479b103c275d2db2d6c199fdc6f589b29b742f6405e01cc5a9a1d135d"))
}
}
// MARK: - Sodium
describe("an extended Sodium") {
var sodium: Sodium!
var genericHash: GenericHashType!
beforeEach {
sodium = Sodium()
genericHash = sodium.genericHash
}
context("when generating a blinding factor") {
it("successfully generates a blinding factor") {
let result = sodium.generateBlindingFactor(
serverPublicKey: TestConstants.serverPublicKey,
genericHash: genericHash
)
expect(result?.toHexString())
.to(equal("84e3eb75028a9b73fec031b7448e322a68ca6485fad81ab1bead56f759ebeb0f"))
}
it("fails if the serverPublicKey is not a hex string") {
let result = sodium.generateBlindingFactor(
serverPublicKey: "Test",
genericHash: genericHash
)
expect(result).to(beNil())
}
it("fails if it cannot hash the serverPublicKey bytes") {
genericHash = MockGenericHash()
(genericHash as? MockGenericHash)?
.when { $0.hash(message: anyArray(), outputLength: any()) }
.thenReturn(nil)
let result = sodium.generateBlindingFactor(
serverPublicKey: TestConstants.serverPublicKey,
genericHash: genericHash
)
expect(result).to(beNil())
}
}
context("when generating a blinded key pair") {
it("successfully generates a blinded key pair") {
let result = sodium.blindedKeyPair(
serverPublicKey: TestConstants.serverPublicKey,
edKeyPair: Box.KeyPair(
publicKey: Data(hex: TestConstants.edPublicKey).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
),
genericHash: genericHash
)
// Note: The first 64 characters of the secretKey are consistent but the chars after that always differ
expect(result?.publicKey.toHexString()).to(equal(TestConstants.blindedPublicKey))
expect(String(result?.secretKey.toHexString().prefix(64) ?? ""))
.to(equal("16663322d6b684e1c9dcc02b9e8642c3affd3bc431a9ea9e63dbbac88ce7a305"))
}
it("fails if the edKeyPair public key length wrong") {
let result = sodium.blindedKeyPair(
serverPublicKey: TestConstants.serverPublicKey,
edKeyPair: Box.KeyPair(
publicKey: Data(hex: String(TestConstants.edPublicKey.prefix(4))).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
),
genericHash: genericHash
)
expect(result).to(beNil())
}
it("fails if the edKeyPair secret key length wrong") {
let result = sodium.blindedKeyPair(
serverPublicKey: TestConstants.serverPublicKey,
edKeyPair: Box.KeyPair(
publicKey: Data(hex: TestConstants.edPublicKey).bytes,
secretKey: Data(hex: String(TestConstants.edSecretKey.prefix(4))).bytes
),
genericHash: genericHash
)
expect(result).to(beNil())
}
it("fails if it cannot generate a blinding factor") {
let result = sodium.blindedKeyPair(
serverPublicKey: "Test",
edKeyPair: Box.KeyPair(
publicKey: Data(hex: TestConstants.edPublicKey).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
),
genericHash: genericHash
)
expect(result).to(beNil())
}
}
context("when generating a sogsSignature") {
it("generates a correct signature") {
let result = sodium.sogsSignature(
message: "TestMessage".bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes,
blindedSecretKey: Data(hex: "44d82cc15c0a5056825cae7520b6b52d000a23eb0c5ed94c4be2d9dc41d2d409").bytes,
blindedPublicKey: Data(hex: "0bb7815abb6ba5142865895f3e5286c0527ba4d31dbb75c53ce95e91ffe025a2").bytes
)
expect(result?.toHexString())
.to(equal(
"dcc086abdd2a740d9260b008fb37e12aa0ff47bd2bd9e177bbbec37fd46705a9" +
"072ce747bda66c788c3775cdd7ad60ad15a478e0886779aad5d795fd7bf8350d"
))
}
}
context("when combining keys") {
it("generates a correct combined key") {
let result = sodium.combineKeys(
lhsKeyBytes: Data(hex: TestConstants.edSecretKey).bytes,
rhsKeyBytes: Data(hex: TestConstants.edPublicKey).bytes
)
expect(result?.toHexString())
.to(equal("1159b5d0fcfba21228eb2121a0f59712fa8276fc6e5547ff519685a40b9819e6"))
}
it("fails if the scalar multiplication fails") {
let result = sodium.combineKeys(
lhsKeyBytes: sodium.generatePrivateKeyScalar(secretKey: Data(hex: TestConstants.edSecretKey).bytes),
rhsKeyBytes: Data(hex: TestConstants.publicKey).bytes
)
expect(result).to(beNil())
}
}
context("when creating a shared blinded encryption key") {
it("generates a correct combined key") {
let result = sodium.sharedBlindedEncryptionKey(
secretKey: Data(hex: TestConstants.edSecretKey).bytes,
otherBlindedPublicKey: Data(hex: TestConstants.blindedPublicKey).bytes,
fromBlindedPublicKey: Data(hex: TestConstants.publicKey).bytes,
toBlindedPublicKey: Data(hex: TestConstants.blindedPublicKey).bytes,
genericHash: genericHash
)
expect(result?.toHexString())
.to(equal("388ee09e4c356b91f1cce5cc0aa0cf59e8e8cade69af61685d09c2d2731bc99e"))
}
it("fails if the scalar multiplication fails") {
let result = sodium.sharedBlindedEncryptionKey(
secretKey: Data(hex: TestConstants.edSecretKey).bytes,
otherBlindedPublicKey: Data(hex: TestConstants.publicKey).bytes,
fromBlindedPublicKey: Data(hex: TestConstants.edPublicKey).bytes,
toBlindedPublicKey: Data(hex: TestConstants.publicKey).bytes,
genericHash: genericHash
)
expect(result?.toHexString()).to(beNil())
}
}
context("when checking if a session id matches a blinded id") {
it("returns true when they match") {
let result = sodium.sessionId(
"05\(TestConstants.publicKey)",
matchesBlindedId: "15\(TestConstants.blindedPublicKey)",
serverPublicKey: TestConstants.serverPublicKey,
genericHash: genericHash
)
expect(result).to(beTrue())
}
it("returns false if given an invalid session id") {
let result = sodium.sessionId(
"AB\(TestConstants.publicKey)",
matchesBlindedId: "15\(TestConstants.blindedPublicKey)",
serverPublicKey: TestConstants.serverPublicKey,
genericHash: genericHash
)
expect(result).to(beFalse())
}
it("returns false if given an invalid blinded id") {
let result = sodium.sessionId(
"05\(TestConstants.publicKey)",
matchesBlindedId: "AB\(TestConstants.blindedPublicKey)",
serverPublicKey: TestConstants.serverPublicKey,
genericHash: genericHash
)
expect(result).to(beFalse())
}
it("returns false if it fails to generate the blinding factor") {
let result = sodium.sessionId(
"05\(TestConstants.publicKey)",
matchesBlindedId: "15\(TestConstants.blindedPublicKey)",
serverPublicKey: "Test",
genericHash: genericHash
)
expect(result).to(beFalse())
}
}
}
// MARK: - GenericHash
describe("an extended GenericHash") {
var genericHash: GenericHashType!
beforeEach {
genericHash = Sodium().genericHash
}
context("when generating a hash with salt and personal values") {
it("generates a hash correctly") {
let result = genericHash.hashSaltPersonal(
message: "TestMessage".bytes,
outputLength: 32,
key: "Key".bytes,
salt: "Salt".bytes,
personal: "Personal".bytes
)
expect(result).toNot(beNil())
expect(result?.count).to(equal(32))
}
it("generates a hash correctly with no key") {
let result = genericHash.hashSaltPersonal(
message: "TestMessage".bytes,
outputLength: 32,
key: nil,
salt: "Salt".bytes,
personal: "Personal".bytes
)
expect(result).toNot(beNil())
expect(result?.count).to(equal(32))
}
it("fails if given invalid options") {
let result = genericHash.hashSaltPersonal(
message: "TestMessage".bytes,
outputLength: 65, // Max of 64
key: "Key".bytes,
salt: "Salt".bytes,
personal: "Personal".bytes
)
expect(result).to(beNil())
}
}
}
// MARK: - AeadXChaCha20Poly1305IetfType
describe("an extended AeadXChaCha20Poly1305IetfType") {
var aeadXchacha20poly1305ietf: AeadXChaCha20Poly1305IetfType!
beforeEach {
aeadXchacha20poly1305ietf = Sodium().aead.xchacha20poly1305ietf
}
context("when encrypting") {
it("encrypts correctly") {
let result = aeadXchacha20poly1305ietf.encrypt(
message: "TestMessage".bytes,
secretKey: Data(hex: TestConstants.publicKey).bytes,
nonce: "TestNonce".bytes,
additionalData: nil
)
expect(result).toNot(beNil())
expect(result?.count).to(equal(27))
}
it("encrypts correctly with additional data") {
let result = aeadXchacha20poly1305ietf.encrypt(
message: "TestMessage".bytes,
secretKey: Data(hex: TestConstants.publicKey).bytes,
nonce: "TestNonce".bytes,
additionalData: "TestData".bytes
)
expect(result).toNot(beNil())
expect(result?.count).to(equal(27))
}
it("fails if given an invalid key") {
let result = aeadXchacha20poly1305ietf.encrypt(
message: "TestMessage".bytes,
secretKey: "TestKey".bytes,
nonce: "TestNonce".bytes,
additionalData: "TestData".bytes
)
expect(result).to(beNil())
}
}
}
}
}

View File

@ -11,9 +11,10 @@ extension Dependencies {
identityManager: IdentityManagerProtocol? = nil,
storage: SessionMessagingKitStorageProtocol? = nil,
sodium: SodiumType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
sign: SignType? = nil,
box: BoxType? = nil,
genericHash: GenericHashType? = nil,
sign: SignType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
ed25519: Ed25519Type? = nil,
nonceGenerator16: NonceGenerator16ByteType? = nil,
nonceGenerator24: NonceGenerator24ByteType? = nil,
@ -25,9 +26,10 @@ extension Dependencies {
identityManager: (identityManager ?? self._identityManager),
storage: (storage ?? self._storage),
sodium: (sodium ?? self._sodium),
aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf),
sign: (sign ?? self._sign),
box: (box ?? self._box),
genericHash: (genericHash ?? self._genericHash),
sign: (sign ?? self._sign),
aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf),
ed25519: (ed25519 ?? self._ed25519),
nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16),
nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24),

View File

@ -0,0 +1,17 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Sodium
@testable import SessionMessagingKit
class MockBox: Mock<BoxType>, BoxType {
func seal(message: Bytes, recipientPublicKey: Bytes) -> Bytes? {
return accept(args: [message, recipientPublicKey]) as? Bytes
}
func open(anonymousCipherText: Bytes, recipientPublicKey: Bytes, recipientSecretKey: Bytes) -> Bytes? {
return accept(args: [anonymousCipherText, recipientPublicKey, recipientSecretKey]) as? Bytes
}
}

View File

@ -7,6 +7,7 @@ import Sodium
@testable import SessionMessagingKit
class MockSign: Mock<SignType>, SignType {
var Bytes: Int = 64
var PublicKeyBytes: Int = 32
func signature(message: Bytes, secretKey: Bytes) -> Bytes? {

View File

@ -7,12 +7,13 @@ import Sodium
@testable import SessionMessagingKit
class MockSodium: Mock<SodiumType>, SodiumType {
func getBox() -> BoxType { return accept() as! BoxType }
func getGenericHash() -> GenericHashType { return accept() as! GenericHashType }
func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return accept() as! AeadXChaCha20Poly1305IetfType }
func getSign() -> SignType { return accept() as! SignType }
func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return accept() as! AeadXChaCha20Poly1305IetfType }
func generateBlindingFactor(serverPublicKey: String) -> Bytes? {
return accept(args: [serverPublicKey]) as? Bytes
func generateBlindingFactor(serverPublicKey: String, genericHash: GenericHashType) -> Bytes? {
return accept(args: [serverPublicKey, genericHash]) as? Bytes
}
func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair? {
@ -31,7 +32,7 @@ class MockSodium: Mock<SodiumType>, SodiumType {
return accept(args: [a, otherBlindedPublicKey, kA, kB, genericHash]) as? Bytes
}
func sessionId(_ sessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String) -> Bool {
return accept(args: [sessionId, blindedSessionId, serverPublicKey]) as! Bool
func sessionId(_ sessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String, genericHash: GenericHashType) -> Bool {
return accept(args: [sessionId, blindedSessionId, serverPublicKey, genericHash]) as! Bool
}
}

View File

@ -12,9 +12,10 @@ extension OpenGroupManager.OGMDependencies {
identityManager: IdentityManagerProtocol? = nil,
storage: SessionMessagingKitStorageProtocol? = nil,
sodium: SodiumType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
sign: SignType? = nil,
box: BoxType? = nil,
genericHash: GenericHashType? = nil,
sign: SignType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
ed25519: Ed25519Type? = nil,
nonceGenerator16: NonceGenerator16ByteType? = nil,
nonceGenerator24: NonceGenerator24ByteType? = nil,
@ -27,9 +28,10 @@ extension OpenGroupManager.OGMDependencies {
identityManager: (identityManager ?? self._identityManager),
storage: (storage ?? self._storage),
sodium: (sodium ?? self._sodium),
aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf),
sign: (sign ?? self._sign),
box: (box ?? self._box),
genericHash: (genericHash ?? self._genericHash),
sign: (sign ?? self._sign),
aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf),
ed25519: (ed25519 ?? self._ed25519),
nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16),
nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24),

View File

@ -11,6 +11,7 @@ extension String {
.map { index -> String in String(chars[index]) + String(chars[index + 1]) }
.compactMap { (str: String) -> UInt8? in UInt8(str, radix: 16) }
guard bytes.count > 0 else { return nil }
guard (self.count / bytes.count) == 2 else { return nil }
return Data(bytes)

View File

@ -42,11 +42,11 @@ class SessionIdSpec: QuickSpec {
it("generates the correct hex string") {
expect(SessionId(.unblinded, publicKey: Data(hex: TestConstants.publicKey).bytes).hexString)
.to(equal("007aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"))
.to(equal("0088672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
expect(SessionId(.standard, publicKey: Data(hex: TestConstants.publicKey).bytes).hexString)
.to(equal("057aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"))
.to(equal("0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
expect(SessionId(.blinded, publicKey: Data(hex: TestConstants.publicKey).bytes).hexString)
.to(equal("157aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"))
.to(equal("1588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
}
}

View File

@ -3,8 +3,12 @@
import Foundation
enum TestConstants {
// Test Private key, not actually used (from here https://www.notion.so/oxen/SOGS-Authentication-dc64cc846cb24b2abbf7dd4bfd74abbb)
static let publicKey: String = "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"
static let privateKey: String = "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4"
static let edSecretKey: String = "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4"
// Test keys (from here https://github.com/jagerman/session-pysogs/blob/docs/contrib/auth-example.py)
static let publicKey: String = "88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"
static let privateKey: String = "30d796c1ddb4dc455fd998a98aa275c247494a9a7bde9c1fee86ae45cd585241"
static let edKeySeed: String = "c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9"
static let edPublicKey: String = "bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc"
static let edSecretKey: String = "c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc"
static let blindedPublicKey: String = "98932d4bccbe595a8789d7eb1629cefc483a0eaddc7e20e8fe5c771efafd9af5"
static let serverPublicKey: String = "c3b3c6f32f0ab5a57f853cc4f30f5da7fda5624b0c77b3fb0829de562ada081d"
}