Skip to content

Commit

Permalink
Merge pull request #1307 from ably/feature/1305-symmetric-decryption
Browse files Browse the repository at this point in the history
Feature/1305 symmetric decryption
  • Loading branch information
maratal committed Apr 10, 2022
2 parents 8671b61 + 826f6fe commit c89c67b
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 4 deletions.
13 changes: 13 additions & 0 deletions Source/ARTMessage.h
Expand Up @@ -2,6 +2,7 @@

#import <Ably/ARTBaseMessage.h>
#import <Ably/ARTTypes.h>
#import <Ably/ARTChannelOptions.h>

NS_ASSUME_NONNULL_BEGIN

Expand All @@ -16,4 +17,16 @@ NS_ASSUME_NONNULL_BEGIN

@end

@interface ARTMessage (Decoding)

+ (nullable instancetype)fromEncoded:(NSDictionary *)jsonObject
channelOptions:(ARTChannelOptions *)options
error:(NSError *_Nullable *_Nullable)error;

+ (nullable NSArray<ARTMessage *> *)fromEncodedArray:(NSArray<NSDictionary *> *)jsonArray
channelOptions:(ARTChannelOptions *)options
error:(NSError *_Nullable *_Nullable)error;

@end

NS_ASSUME_NONNULL_END
74 changes: 74 additions & 0 deletions Source/ARTMessage.m
@@ -1,4 +1,8 @@
#import "ARTMessage.h"
#import "ARTJsonEncoder.h"
#import "ARTJsonLikeEncoder.h"
#import "ARTBaseMessage+Private.h"
#import "ARTNSArray+ARTFunctional.h"

@implementation ARTMessage

Expand Down Expand Up @@ -45,3 +49,73 @@ - (NSInteger)messageSize {
}

@end

@implementation ARTMessage (Decoding)

+ (instancetype)fromEncoded:(NSDictionary *)jsonObject channelOptions:(ARTChannelOptions *)options error:(NSError **)error {
ARTJsonLikeEncoder *jsonEncoder = [[ARTJsonLikeEncoder alloc] initWithDelegate:[[ARTJsonEncoder alloc] init]];
NSError *encoderError = nil;
ARTDataEncoder *decoder = [[ARTDataEncoder alloc] initWithCipherParams:options.cipher error:&encoderError];
if (encoderError != nil) {
if (error != nil) {
ARTErrorInfo *errorInfo =
[ARTErrorInfo wrap:[ARTErrorInfo createWithCode:ARTErrorUnableToDecodeMessage message:encoderError.localizedFailureReason]
prepend:[NSString stringWithFormat:@"Decoder can't be created with cipher: %@", options.cipher]];
*error = errorInfo;
}
return nil;
}

ARTMessage *message = [jsonEncoder messageFromDictionary:jsonObject];

NSError *decodeError = nil;
message = [message decodeWithEncoder:decoder error:&decodeError];
if (decodeError != nil) {
if (error != nil) {
ARTErrorInfo *errorInfo =
[ARTErrorInfo wrap:[ARTErrorInfo createWithCode:ARTErrorUnableToDecodeMessage message:decodeError.localizedFailureReason]
prepend:[NSString stringWithFormat:@"Failed to decode data for message: %@. Decoding array aborted.", message.name]];
*error = errorInfo;
}
return nil;
}
return message;
}

+ (NSArray<ARTMessage *> *)fromEncodedArray:(NSArray<NSDictionary *> *)jsonArray channelOptions:(ARTChannelOptions *)options error:(NSError **)error {
ARTJsonLikeEncoder *jsonEncoder = [[ARTJsonLikeEncoder alloc] initWithDelegate:[[ARTJsonEncoder alloc] init]];
NSError *encoderError = nil;
ARTDataEncoder *decoder = [[ARTDataEncoder alloc] initWithCipherParams:options.cipher error:&encoderError];
if (encoderError != nil) {
if (error != nil) {
ARTErrorInfo *errorInfo =
[ARTErrorInfo wrap:[ARTErrorInfo createWithCode:ARTErrorUnableToDecodeMessage message:encoderError.localizedFailureReason]
prepend:[NSString stringWithFormat:@"Decoder can't be created with cipher: %@", options.cipher]];
*error = errorInfo;
}
return nil;
}

NSArray<ARTMessage *> *messages = [jsonEncoder messagesFromArray:jsonArray];

NSMutableArray<ARTMessage *> *decodedMessages = [NSMutableArray array];
for (ARTMessage *message in messages) {
NSError *decodeError = nil;
ARTMessage *decodedMessage = [message decodeWithEncoder:decoder error:&decodeError];
if (decodeError != nil) {
if (error != nil) {
ARTErrorInfo *errorInfo =
[ARTErrorInfo wrap:[ARTErrorInfo createWithCode:ARTErrorUnableToDecodeMessage message:decodeError.localizedFailureReason]
prepend:[NSString stringWithFormat:@"Failed to decode data for message: %@. Decoding array aborted.", message.name]];
*error = errorInfo;
}
break;
}
else {
[decodedMessages addObject:decodedMessage];
}
}
return decodedMessages;
}

@end
13 changes: 9 additions & 4 deletions Spec/Test Utilities/TestUtilities.swift
Expand Up @@ -344,15 +344,20 @@ class AblyTests {
}

}

class func loadCryptoTestData(_ fileName: String) -> (key: Data, iv: Data, items: [CryptoTestItem]) {
class func loadCryptoTestRawData(_ fileName: String) -> (key: Data, iv: Data, jsonItems: [JSON]) {
let file = testResourcesPath + fileName + ".json";
let json = JSON(parseJSON: try! String(contentsOfFile: pathForTestResource(file)))

let keyData = Data(base64Encoded: json["key"].stringValue, options: Data.Base64DecodingOptions(rawValue: 0))!
let ivData = Data(base64Encoded: json["iv"].stringValue, options: Data.Base64DecodingOptions(rawValue: 0))!
let items = json["items"].map{ $0.1 }.map(CryptoTestItem.init)


return (keyData, ivData, json["items"].arrayValue)
}

class func loadCryptoTestData(_ fileName: String) -> (key: Data, iv: Data, items: [CryptoTestItem]) {
let (keyData, ivData, jsonItems) = loadCryptoTestRawData(fileName)
let items = jsonItems.map{ $0 }.map(CryptoTestItem.init)
return (keyData, ivData, items)
}
}
Expand Down
57 changes: 57 additions & 0 deletions Spec/Tests/CryptoTests.swift
Expand Up @@ -193,6 +193,55 @@ class CryptoTests: XCTestCase {
try test__should_decrypt_messages_as_expected_in_the_fixtures()
}
}

func reusableTestsTestManualDecryption(fileName: String, expectedEncryptedEncoding: String, keyLength: UInt) throws {
let (key, iv, jsonItems) = AblyTests.loadCryptoTestRawData(fileName)
let decoder = ARTDataEncoder(cipherParams: nil, error: nil)
let cipherParams = ARTCipherParams(algorithm: "aes", key: key as ARTCipherKeyCompatible, iv: iv)
let channelOptions = ARTChannelOptions(cipher: cipherParams)

func extractMessage(_ rawFixture: JSON) -> ARTMessage {
let msg = ARTMessage(name: rawFixture["name"].stringValue, data: rawFixture["data"].stringValue)
msg.encoding = rawFixture["encoding"].stringValue
return msg
}

// one by one
for jsonItem in jsonItems {
let fixture = extractMessage(jsonItem["encoded"])
let encryptedFixture = jsonItem["encrypted"]
expect(encryptedFixture["encoding"].stringValue).to(endWith("\(expectedEncryptedEncoding)/base64"))

var error: NSError?
let decoded = fixture.decode(with: decoder, error: &error) as! ARTMessage
expect(error).to(beNil())
expect(decoded).notTo(beNil())

let rawDictionary = try XCTUnwrap(encryptedFixture.dictionaryObject)
let decrypted = try XCTUnwrap(ARTMessage.fromEncoded(rawDictionary, channelOptions: channelOptions))
expect(decrypted).notTo(beNil())

expect(decrypted).to(equal(decoded))
}

// a bunch at once
let encryptedFixtures = try jsonItems.map { try XCTUnwrap($0["encrypted"].dictionaryObject) }

let decryptedArray = try XCTUnwrap(ARTMessage.fromEncodedArray(encryptedFixtures, channelOptions: channelOptions))
expect(decryptedArray.count).to(equal(jsonItems.count))

for i in 0..<jsonItems.count {
let fixture = extractMessage(jsonItems[i]["encoded"])

var error: NSError?
let decoded = fixture.decode(with: decoder, error: &error) as! ARTMessage
expect(error).to(beNil())
expect(decoded).notTo(beNil())

let decrypted = decryptedArray[i]
expect(decrypted).to(equal(decoded))
}
}

func reusableTestsWrapper__Crypto__with_fixtures_from_crypto_data_128_json__reusableTestsTestFixture(testCase: TestCase_ReusableTestsTestFixture) throws {
try reusableTestsTestFixture(("crypto-data-128", "cipher+aes-128-cbc", 128), testCase: testCase)
Expand All @@ -217,4 +266,12 @@ class CryptoTests: XCTestCase {
func test__014__Crypto__with_fixtures_from_crypto_data_256_json__should_decrypt_messages_as_expected_in_the_fixtures() throws {
try reusableTestsWrapper__Crypto__with_fixtures_from_crypto_data_256_json__reusableTestsTestFixture(testCase: .should_decrypt_messages_as_expected_in_the_fixtures)
}

func test__015__Crypto__with_fixtures_from_crypto_data_128_json__manual_decrypt_messages_as_expected_in_the_fixtures() throws {
try reusableTestsTestManualDecryption(fileName: "crypto-data-128", expectedEncryptedEncoding: "cipher+aes-128-cbc", keyLength: 128)
}

func test__016__Crypto__with_fixtures_from_crypto_data_256_json__manual_decrypt_messages_as_expected_in_the_fixtures() throws {
try reusableTestsTestManualDecryption(fileName: "crypto-data-256", expectedEncryptedEncoding: "cipher+aes-256-cbc", keyLength: 256)
}
}

0 comments on commit c89c67b

Please sign in to comment.