From cc4d85a91ea2233cac6a3e8e1df634f17972856a Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 19 Dec 2018 18:27:50 -0500 Subject: [PATCH] Encrypted device names. --- Pods | 2 +- Signal/test/views/ImageEditorTest.swift | 114 +++++++++ SignalServiceKit/protobuf/SignalIOS.proto | 10 + SignalServiceKit/src/Devices/OWSDevice.m | 22 ++ .../src/Protos/Generated/SignalIOS.pb.swift | 85 +++++++ .../src/Protos/Generated/SignalIOSProto.swift | 127 ++++++++++ SignalServiceKit/src/Util/DeviceNames.swift | 220 ++++++++++++++++++ .../tests/Util/DeviceNamesTest.swift | 90 +++++++ .../tests/Util/JobQueueTest.swift | 5 - 9 files changed, 669 insertions(+), 6 deletions(-) create mode 100644 Signal/test/views/ImageEditorTest.swift create mode 100644 SignalServiceKit/src/Util/DeviceNames.swift create mode 100644 SignalServiceKit/tests/Util/DeviceNamesTest.swift diff --git a/Pods b/Pods index 71a3d8c660..d74b4688bd 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit 71a3d8c6605e1fa934b37c6d0dbad2b4d03e75e1 +Subproject commit d74b4688bd6876a7038d30b0ce6ae3f24844ff88 diff --git a/Signal/test/views/ImageEditorTest.swift b/Signal/test/views/ImageEditorTest.swift new file mode 100644 index 0000000000..343e168a6e --- /dev/null +++ b/Signal/test/views/ImageEditorTest.swift @@ -0,0 +1,114 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import XCTest +@testable import Signal +@testable import SignalMessaging + +extension ImageEditorModel { + func itemIds() -> [String] { + return items().map { (item) in + item.itemId + } + } +} + +class ImageEditorTest: SignalBaseTest { + + override func setUp() { + super.setUp() + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testImageEditorContents() { + let imagePath = writeDummyImage() + + let contents = ImageEditorContents(imagePath: imagePath, + imageSizePixels: CGSize(width: 1, height: 1)) + XCTAssertEqual(0, contents.itemMap.count) + + let item = ImageEditorItem(itemType: .test) + contents.append(item: item) + XCTAssertEqual(1, contents.itemMap.count) + + let contentsCopy = contents.clone() + XCTAssertEqual(1, contents.itemMap.count) + XCTAssertEqual(1, contentsCopy.itemMap.count) + + contentsCopy.remove(item: item) + XCTAssertEqual(1, contents.itemMap.count) + XCTAssertEqual(0, contentsCopy.itemMap.count) + + let modifiedItem = ImageEditorItem(itemId: item.itemId, itemType: item.itemType) + contents.replace(item: modifiedItem) + XCTAssertEqual(1, contents.itemMap.count) + XCTAssertEqual(0, contentsCopy.itemMap.count) + } + + private func writeDummyImage() -> String { + let image = UIImage.init(color: .red, size: CGSize(width: 1, height: 1)) + guard let data = UIImagePNGRepresentation(image) else { + owsFail("Couldn't export dummy image.") + } + let filePath = OWSFileSystem.temporaryFilePath(withFileExtension: "png") + try! data.write(to: URL(fileURLWithPath: filePath)) + return filePath + } + + func testImageEditor() { + let imagePath = writeDummyImage() + + let imageEditor = try! ImageEditorModel(srcImagePath: imagePath) + XCTAssertFalse(imageEditor.canUndo()) + XCTAssertFalse(imageEditor.canRedo()) + XCTAssertEqual(0, imageEditor.itemCount()) + + let itemA = ImageEditorItem(itemType: .test) + imageEditor.append(item: itemA) + XCTAssertTrue(imageEditor.canUndo()) + XCTAssertFalse(imageEditor.canRedo()) + XCTAssertEqual(1, imageEditor.itemCount()) + XCTAssertEqual([itemA.itemId], imageEditor.itemIds()) + + imageEditor.undo() + XCTAssertFalse(imageEditor.canUndo()) + XCTAssertTrue(imageEditor.canRedo()) + XCTAssertEqual(0, imageEditor.itemCount()) + + imageEditor.redo() + XCTAssertTrue(imageEditor.canUndo()) + XCTAssertFalse(imageEditor.canRedo()) + XCTAssertEqual(1, imageEditor.itemCount()) + XCTAssertEqual([itemA.itemId], imageEditor.itemIds()) + + imageEditor.undo() + XCTAssertFalse(imageEditor.canUndo()) + XCTAssertTrue(imageEditor.canRedo()) + XCTAssertEqual(0, imageEditor.itemCount()) + + let itemB = ImageEditorItem(itemType: .test) + imageEditor.append(item: itemB) + XCTAssertTrue(imageEditor.canUndo()) + XCTAssertFalse(imageEditor.canRedo()) + XCTAssertEqual(1, imageEditor.itemCount()) + XCTAssertEqual([itemB.itemId], imageEditor.itemIds()) + + let itemC = ImageEditorItem(itemType: .test) + imageEditor.append(item: itemC) + XCTAssertTrue(imageEditor.canUndo()) + XCTAssertFalse(imageEditor.canRedo()) + XCTAssertEqual(2, imageEditor.itemCount()) + XCTAssertEqual([itemB.itemId, itemC.itemId], imageEditor.itemIds()) + + imageEditor.undo() + XCTAssertTrue(imageEditor.canUndo()) + XCTAssertTrue(imageEditor.canRedo()) + XCTAssertEqual(1, imageEditor.itemCount()) + XCTAssertEqual([itemB.itemId], imageEditor.itemIds()) + } +} diff --git a/SignalServiceKit/protobuf/SignalIOS.proto b/SignalServiceKit/protobuf/SignalIOS.proto index dbb406b741..303c067b5b 100644 --- a/SignalServiceKit/protobuf/SignalIOS.proto +++ b/SignalServiceKit/protobuf/SignalIOS.proto @@ -33,3 +33,13 @@ message BackupSnapshot { repeated BackupEntity entity = 1; } + +message DeviceName { + // @required + optional bytes ephemeralPublic = 1; + // @required + optional bytes syntheticIv = 2; + // @required + optional bytes ciphertext = 3; +} + diff --git a/SignalServiceKit/src/Devices/OWSDevice.m b/SignalServiceKit/src/Devices/OWSDevice.m index 00687cd737..54f6ad2cd8 100644 --- a/SignalServiceKit/src/Devices/OWSDevice.m +++ b/SignalServiceKit/src/Devices/OWSDevice.m @@ -13,6 +13,7 @@ #import "YapDatabaseTransaction.h" #import #import +#import #import NS_ASSUME_NONNULL_BEGIN @@ -130,6 +131,13 @@ NSString *const kOWSPrimaryStorage_MayHaveLinkedDevices = @"kTSStorageManager_Ma return TSAccountManager.sharedInstance; } +- (OWSIdentityManager *)identityManager +{ + OWSAssertDebug(SSKEnvironment.shared.identityManager); + + return SSKEnvironment.shared.identityManager; +} + #pragma mark - - (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction @@ -275,6 +283,20 @@ NSString *const kOWSPrimaryStorage_MayHaveLinkedDevices = @"kTSStorageManager_Ma - (NSString *)displayName { if (self.name) { + ECKeyPair *_Nullable identityKeyPair = self.identityManager.identityKeyPair; + OWSAssertDebug(identityKeyPair); + if (identityKeyPair) { + NSError *error; + NSString *_Nullable decryptedName = + [DeviceNames decryptDeviceNameWithBase64String:self.name identityKeyPair:identityKeyPair error:&error]; + if (error) { + // Not necessarily an error; might be a legacy device name. + OWSLogError(@"Could not decrypt device name: %@", error); + } else if (decryptedName) { + return decryptedName; + } + } + return self.name; } diff --git a/SignalServiceKit/src/Protos/Generated/SignalIOS.pb.swift b/SignalServiceKit/src/Protos/Generated/SignalIOS.pb.swift index b2dfd5fb42..9f2423946e 100644 --- a/SignalServiceKit/src/Protos/Generated/SignalIOS.pb.swift +++ b/SignalServiceKit/src/Protos/Generated/SignalIOS.pb.swift @@ -132,6 +132,50 @@ struct IOSProtos_BackupSnapshot { init() {} } +struct IOSProtos_DeviceName { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var ephemeralPublic: Data { + get {return _ephemeralPublic ?? SwiftProtobuf.Internal.emptyData} + set {_ephemeralPublic = newValue} + } + /// Returns true if `ephemeralPublic` has been explicitly set. + var hasEphemeralPublic: Bool {return self._ephemeralPublic != nil} + /// Clears the value of `ephemeralPublic`. Subsequent reads from it will return its default value. + mutating func clearEphemeralPublic() {self._ephemeralPublic = nil} + + /// @required + var syntheticIv: Data { + get {return _syntheticIv ?? SwiftProtobuf.Internal.emptyData} + set {_syntheticIv = newValue} + } + /// Returns true if `syntheticIv` has been explicitly set. + var hasSyntheticIv: Bool {return self._syntheticIv != nil} + /// Clears the value of `syntheticIv`. Subsequent reads from it will return its default value. + mutating func clearSyntheticIv() {self._syntheticIv = nil} + + /// @required + var ciphertext: Data { + get {return _ciphertext ?? SwiftProtobuf.Internal.emptyData} + set {_ciphertext = newValue} + } + /// Returns true if `ciphertext` has been explicitly set. + var hasCiphertext: Bool {return self._ciphertext != nil} + /// Clears the value of `ciphertext`. Subsequent reads from it will return its default value. + mutating func clearCiphertext() {self._ciphertext = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _ephemeralPublic: Data? = nil + fileprivate var _syntheticIv: Data? = nil + fileprivate var _ciphertext: Data? = nil +} + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "IOSProtos" @@ -222,3 +266,44 @@ extension IOSProtos_BackupSnapshot.BackupEntity.TypeEnum: SwiftProtobuf._ProtoNa 5: .same(proto: "MISC"), ] } + +extension IOSProtos_DeviceName: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DeviceName" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "ephemeralPublic"), + 2: .same(proto: "syntheticIv"), + 3: .same(proto: "ciphertext"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularBytesField(value: &self._ephemeralPublic) + case 2: try decoder.decodeSingularBytesField(value: &self._syntheticIv) + case 3: try decoder.decodeSingularBytesField(value: &self._ciphertext) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._ephemeralPublic { + try visitor.visitSingularBytesField(value: v, fieldNumber: 1) + } + if let v = self._syntheticIv { + try visitor.visitSingularBytesField(value: v, fieldNumber: 2) + } + if let v = self._ciphertext { + try visitor.visitSingularBytesField(value: v, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: IOSProtos_DeviceName, rhs: IOSProtos_DeviceName) -> Bool { + if lhs._ephemeralPublic != rhs._ephemeralPublic {return false} + if lhs._syntheticIv != rhs._syntheticIv {return false} + if lhs._ciphertext != rhs._ciphertext {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/SignalServiceKit/src/Protos/Generated/SignalIOSProto.swift b/SignalServiceKit/src/Protos/Generated/SignalIOSProto.swift index 36c4202b28..5761fbda75 100644 --- a/SignalServiceKit/src/Protos/Generated/SignalIOSProto.swift +++ b/SignalServiceKit/src/Protos/Generated/SignalIOSProto.swift @@ -280,3 +280,130 @@ extension SignalIOSProtoBackupSnapshot.SignalIOSProtoBackupSnapshotBuilder { } #endif + +// MARK: - SignalIOSProtoDeviceName + +@objc public class SignalIOSProtoDeviceName: NSObject { + + // MARK: - SignalIOSProtoDeviceNameBuilder + + @objc public class func builder(ephemeralPublic: Data, syntheticIv: Data, ciphertext: Data) -> SignalIOSProtoDeviceNameBuilder { + return SignalIOSProtoDeviceNameBuilder(ephemeralPublic: ephemeralPublic, syntheticIv: syntheticIv, ciphertext: ciphertext) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SignalIOSProtoDeviceNameBuilder { + let builder = SignalIOSProtoDeviceNameBuilder(ephemeralPublic: ephemeralPublic, syntheticIv: syntheticIv, ciphertext: ciphertext) + return builder + } + + @objc public class SignalIOSProtoDeviceNameBuilder: NSObject { + + private var proto = IOSProtos_DeviceName() + + @objc fileprivate override init() {} + + @objc fileprivate init(ephemeralPublic: Data, syntheticIv: Data, ciphertext: Data) { + super.init() + + setEphemeralPublic(ephemeralPublic) + setSyntheticIv(syntheticIv) + setCiphertext(ciphertext) + } + + @objc public func setEphemeralPublic(_ valueParam: Data) { + proto.ephemeralPublic = valueParam + } + + @objc public func setSyntheticIv(_ valueParam: Data) { + proto.syntheticIv = valueParam + } + + @objc public func setCiphertext(_ valueParam: Data) { + proto.ciphertext = valueParam + } + + @objc public func build() throws -> SignalIOSProtoDeviceName { + return try SignalIOSProtoDeviceName.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SignalIOSProtoDeviceName.parseProto(proto).serializedData() + } + } + + fileprivate let proto: IOSProtos_DeviceName + + @objc public let ephemeralPublic: Data + + @objc public let syntheticIv: Data + + @objc public let ciphertext: Data + + private init(proto: IOSProtos_DeviceName, + ephemeralPublic: Data, + syntheticIv: Data, + ciphertext: Data) { + self.proto = proto + self.ephemeralPublic = ephemeralPublic + self.syntheticIv = syntheticIv + self.ciphertext = ciphertext + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SignalIOSProtoDeviceName { + let proto = try IOSProtos_DeviceName(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: IOSProtos_DeviceName) throws -> SignalIOSProtoDeviceName { + guard proto.hasEphemeralPublic else { + throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: ephemeralPublic") + } + let ephemeralPublic = proto.ephemeralPublic + + guard proto.hasSyntheticIv else { + throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: syntheticIv") + } + let syntheticIv = proto.syntheticIv + + guard proto.hasCiphertext else { + throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: ciphertext") + } + let ciphertext = proto.ciphertext + + // MARK: - Begin Validation Logic for SignalIOSProtoDeviceName - + + // MARK: - End Validation Logic for SignalIOSProtoDeviceName - + + let result = SignalIOSProtoDeviceName(proto: proto, + ephemeralPublic: ephemeralPublic, + syntheticIv: syntheticIv, + ciphertext: ciphertext) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SignalIOSProtoDeviceName { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SignalIOSProtoDeviceName.SignalIOSProtoDeviceNameBuilder { + @objc public func buildIgnoringErrors() -> SignalIOSProtoDeviceName? { + return try! self.build() + } +} + +#endif diff --git a/SignalServiceKit/src/Util/DeviceNames.swift b/SignalServiceKit/src/Util/DeviceNames.swift new file mode 100644 index 0000000000..e828184011 --- /dev/null +++ b/SignalServiceKit/src/Util/DeviceNames.swift @@ -0,0 +1,220 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import Curve25519Kit +import AxolotlKit + +@objc +public enum DeviceNameError: Int, Error { + case assertionFailure + case invalidInput +} + +@objc +public class DeviceNames: NSObject { + // Never instantiate this class. + private override init() {} + + private static let syntheticIVLength: UInt = 16 + + @objc + public class func encryptDeviceName(plaintext: String, + identityKeyPair: ECKeyPair) throws -> Data { + + guard let plaintextData = plaintext.data(using: .utf8) else { + owsFailDebug("Could not convert text to UTF-8.") + throw DeviceNameError.invalidInput + } + + let ephemeralKeyPair = Curve25519.generateKeyPair() + + // master_secret = ECDH(ephemeral_private, identity_public). + let masterSecret: Data + do { + masterSecret = try Curve25519.generateSharedSecret(fromPublicKey: identityKeyPair.publicKey, + privateKey: ephemeralKeyPair.privateKey) + } catch { + Logger.error("Could not generate shared secret: \(error)") + throw error + } + + // synthetic_iv = HmacSHA256(key=HmacSHA256(key=master_secret, input=“auth”), input=plaintext)[0:16] + let syntheticIV = try computeSyntheticIV(masterSecret: masterSecret, + plaintextData: plaintextData) + + // cipher_key = HmacSHA256(key=HmacSHA256(key=master_secret, “cipher”), input=synthetic_iv) + let cipherKey = try computeCipherKey(masterSecret: masterSecret, syntheticIV: syntheticIV) + + // cipher_text = AES-CTR(key=cipher_key, input=plaintext, counter=0) + // + // An all-zeros IV corresponds to an AES CTR counter of zero. + let ciphertextIV = Data(count: Int(kAES256CTR_IVLength)) + guard let ciphertextKey = OWSAES256Key(data: cipherKey) else { + owsFailDebug("Invalid cipher key.") + throw DeviceNameError.assertionFailure + } + guard let ciphertext: AES256CTREncryptionResult = Cryptography.encryptAESCTR(plaintextData: plaintextData, initializationVector: ciphertextIV, key: ciphertextKey) else { + owsFailDebug("Could not encrypt cipher text.") + throw DeviceNameError.assertionFailure + } + + guard let keyData = (ephemeralKeyPair.publicKey as NSData).prependKeyType() else { + owsFailDebug("Could not prepend key type.") + throw DeviceNameError.assertionFailure + } + let protoBuilder = SignalIOSProtoDeviceName.builder(ephemeralPublic: keyData as Data, + syntheticIv: syntheticIV, + ciphertext: ciphertext.ciphertext) + let protoData = try protoBuilder.buildSerializedData() + + // NOTE: This uses Data's foundation method rather than the NSData's SSK method. + let protoDataBase64 = protoData.base64EncodedData() + + return protoDataBase64 + } + + private class func computeSyntheticIV(masterSecret: Data, + plaintextData: Data) throws -> Data { + // synthetic_iv = HmacSHA256(key=HmacSHA256(key=master_secret, input=“auth”), input=plaintext)[0:16] + guard let syntheticIVInput = "auth".data(using: .utf8) else { + owsFailDebug("Could not convert text to UTF-8.") + throw DeviceNameError.assertionFailure + } + guard let syntheticIVKey = Cryptography.computeSHA256HMAC(syntheticIVInput, withHMACKey: masterSecret) else { + owsFailDebug("Could not compute synthetic IV key.") + throw DeviceNameError.assertionFailure + } + guard let syntheticIV = Cryptography.truncatedSHA256HMAC(plaintextData, withHMACKey: syntheticIVKey, truncation: syntheticIVLength) else { + owsFailDebug("Could not compute synthetic IV.") + throw DeviceNameError.assertionFailure + } + return syntheticIV + } + + private class func computeCipherKey(masterSecret: Data, + syntheticIV: Data) throws -> Data { + // cipher_key = HmacSHA256(key=HmacSHA256(key=master_secret, “cipher”), input=synthetic_iv) + guard let cipherKeyInput = "cipher".data(using: .utf8) else { + owsFailDebug("Could not convert text to UTF-8.") + throw DeviceNameError.assertionFailure + } + guard let cipherKeyKey = Cryptography.computeSHA256HMAC(cipherKeyInput, withHMACKey: masterSecret) else { + owsFailDebug("Could not compute cipher key key.") + throw DeviceNameError.assertionFailure + } + guard let cipherKey = Cryptography.computeSHA256HMAC(syntheticIV, withHMACKey: cipherKeyKey) else { + owsFailDebug("Could not compute cipher key.") + throw DeviceNameError.assertionFailure + } + return cipherKey + } + + @objc + public class func decryptDeviceName(base64String: String, + identityKeyPair: ECKeyPair) throws -> String { + + guard let protoData = Data(base64Encoded: base64String) else { + // Not necessarily an error; might be a legacy device name. + throw DeviceNameError.invalidInput + } + + return try decryptDeviceName(protoData: protoData, + identityKeyPair: identityKeyPair) + } + + @objc + public class func decryptDeviceName(base64Data: Data, + identityKeyPair: ECKeyPair) throws -> String { + + guard let protoData = Data(base64Encoded: base64Data) else { + // Not necessarily an error; might be a legacy device name. + throw DeviceNameError.invalidInput + } + + return try decryptDeviceName(protoData: protoData, + identityKeyPair: identityKeyPair) + } + + @objc + public class func decryptDeviceName(protoData: Data, + identityKeyPair: ECKeyPair) throws -> String { + + let proto: SignalIOSProtoDeviceName + do { + proto = try SignalIOSProtoDeviceName.parseData(protoData) + } catch { + // Not necessarily an error; might be a legacy device name. + Logger.error("failed to parse proto") + throw DeviceNameError.invalidInput + } + + let ephemeralPublicData = proto.ephemeralPublic + let receivedSyntheticIV = proto.syntheticIv + let ciphertext = proto.ciphertext + + let ephemeralPublic: Data + do { + ephemeralPublic = try (ephemeralPublicData as NSData).removeKeyType() as Data + } catch { + owsFailDebug("failed to remove key type") + throw DeviceNameError.invalidInput + } + + guard ephemeralPublic.count > 0 else { + owsFailDebug("Invalid ephemeral public.") + throw DeviceNameError.assertionFailure + } + guard receivedSyntheticIV.count == syntheticIVLength else { + owsFailDebug("Invalid synthetic IV.") + throw DeviceNameError.assertionFailure + } + guard ciphertext.count > 0 else { + owsFailDebug("Invalid cipher text.") + throw DeviceNameError.assertionFailure + } + + // master_secret = ECDH(identity_private, ephemeral_public) + let masterSecret: Data + do { + masterSecret = try Curve25519.generateSharedSecret(fromPublicKey: ephemeralPublic, + privateKey: identityKeyPair.privateKey) + } catch { + Logger.error("Could not generate shared secret: \(error)") + throw error + } + + // cipher_key = HmacSHA256(key=HmacSHA256(key=master_secret, input=“cipher”), input=synthetic_iv) + let cipherKey = try computeCipherKey(masterSecret: masterSecret, syntheticIV: receivedSyntheticIV) + + // plaintext = AES-CTR(key=cipher_key, input=ciphertext, counter=0) + // + // An all-zeros IV corresponds to an AES CTR counter of zero. + let ciphertextIV = Data(count: Int(kAES256CTR_IVLength)) + guard let ciphertextKey = OWSAES256Key(data: cipherKey) else { + owsFailDebug("Invalid cipher key.") + throw DeviceNameError.assertionFailure + } + guard let plaintextData = Cryptography.decryptAESCTR(cipherText: ciphertext, initializationVector: ciphertextIV, key: ciphertextKey) else { + owsFailDebug("Could not decrypt cipher text.") + throw DeviceNameError.assertionFailure + } + + // Verify the synthetic IV was correct. + // constant_time_compare(HmacSHA256(key=HmacSHA256(key=master_secret, input=”auth”), input=plaintext)[0:16], synthetic_iv) == true + let computedSyntheticIV = try computeSyntheticIV(masterSecret: masterSecret, + plaintextData: plaintextData) + guard receivedSyntheticIV.ows_constantTimeIsEqual(to: computedSyntheticIV) else { + owsFailDebug("Synthetic IV did not match.") + throw DeviceNameError.assertionFailure + } + + guard let plaintext = String(bytes: plaintextData, encoding: .utf8) else { + owsFailDebug("Invalid plaintext.") + throw DeviceNameError.invalidInput + } + + return plaintext + } +} diff --git a/SignalServiceKit/tests/Util/DeviceNamesTest.swift b/SignalServiceKit/tests/Util/DeviceNamesTest.swift new file mode 100644 index 0000000000..a8ca372de8 --- /dev/null +++ b/SignalServiceKit/tests/Util/DeviceNamesTest.swift @@ -0,0 +1,90 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import XCTest +import Curve25519Kit + +@testable import SignalServiceKit + +class DeviceNamesTest: SSKBaseTestSwift { + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + // MARK: + + func testNotEncrypted1() { + + let identityKeyPair = Curve25519.generateKeyPair() + + let plaintext = "alice" + guard let plaintextData = plaintext.data(using: .utf8) else { + XCTFail("Could not convert text to UTF-8.") + return + } + + do { + _ = try DeviceNames.decryptDeviceName(base64Data: plaintextData, + identityKeyPair: identityKeyPair) + XCTFail("Unexpectedly did not throw error.") + } catch DeviceNameError.invalidInput { + // Expected error. + } catch { + owsFailDebug("Unexpected \(error)") + } + } + + func testNotEncrypted2() { + + let identityKeyPair = Curve25519.generateKeyPair() + + let plaintext = "alice" + guard let plaintextData = plaintext.data(using: .utf8) else { + XCTFail("Could not convert text to UTF-8.") + return + } + let base64Data = plaintextData.base64EncodedData() + + do { + _ = try DeviceNames.decryptDeviceName(base64Data: base64Data, + identityKeyPair: identityKeyPair) + XCTFail("Unexpectedly did not throw error.") + } catch DeviceNameError.invalidInput { + // Expected error. + } catch { + owsFailDebug("Unexpected \(error)") + } + } + + func testSimple() { + + let identityKeyPair = Curve25519.generateKeyPair() + + let plaintext = "alice" + let encrypted: Data + do { + encrypted = try DeviceNames.encryptDeviceName(plaintext: plaintext, + identityKeyPair: identityKeyPair) + } catch { + XCTFail("Failed with error: \(error)") + return + } + + let decrypted: String + do { + decrypted = try DeviceNames.decryptDeviceName(base64Data: encrypted, + identityKeyPair: identityKeyPair) + } catch { + XCTFail("Failed with error: \(error)") + return + } + XCTAssertEqual(plaintext, decrypted) + } +} diff --git a/SignalServiceKit/tests/Util/JobQueueTest.swift b/SignalServiceKit/tests/Util/JobQueueTest.swift index a76c1c4847..8516f8c082 100644 --- a/SignalServiceKit/tests/Util/JobQueueTest.swift +++ b/SignalServiceKit/tests/Util/JobQueueTest.swift @@ -3,11 +3,6 @@ // import Foundation - -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - import XCTest @testable import SignalServiceKit