mirror of https://github.com/oxen-io/session-ios
Encrypted device names.
parent
74274e58de
commit
cc4d85a91e
@ -1 +1 @@
|
|||||||
Subproject commit 71a3d8c6605e1fa934b37c6d0dbad2b4d03e75e1
|
Subproject commit d74b4688bd6876a7038d30b0ce6ae3f24844ff88
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue