// // Copyright 2019 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import CommonCrypto import Foundation import LibSignalClient public struct ProvisionMessage { public let aci: Aci? public let phoneNumber: String public let pni: Pni? public let aciIdentityKeyPair: ECKeyPair public let pniIdentityKeyPair: ECKeyPair public let profileKey: OWSAES256Key public let masterKey: Data public let areReadReceiptsEnabled: Bool? public let primaryUserAgent: String? public let provisioningCode: String public let provisioningVersion: UInt32? } public enum ProvisioningError: Error { case invalidProvisionMessage(_ description: String) } public class ProvisioningCipher { public var secondaryDevicePublicKey: PublicKey { return secondaryDeviceKeyPair.publicKey } let secondaryDeviceKeyPair: IdentityKeyPair init(secondaryDeviceKeyPair: IdentityKeyPair) { self.secondaryDeviceKeyPair = secondaryDeviceKeyPair } public class func generate() -> ProvisioningCipher { return ProvisioningCipher(secondaryDeviceKeyPair: IdentityKeyPair.generate()) } internal class var messageInfo: String { return "TextSecure Provisioning Message" } // MARK: public func decrypt(envelope: ProvisioningProtoProvisionEnvelope) throws -> ProvisionMessage { let primaryDeviceEphemeralPublicKey = try PublicKey(envelope.publicKey) let bytes = [UInt8](envelope.body) let versionLength = 1 let ivLength = 16 let macLength = 32 let provisionMessageLength = bytes.count - versionLength - ivLength - macLength guard provisionMessageLength > 0 else { throw ProvisioningError.invalidProvisionMessage("provisioning message too short.") } let version = bytes[0] guard version == 1 else { throw ProvisioningError.invalidProvisionMessage("Unexpected version on provisioning message: \(bytes[0])") } let iv = Array(bytes[1..<17]) let theirMac = bytes.suffix(32) let messageToAuthenticate = bytes[0..<(bytes.count - 32)] let ciphertext = Array(bytes[17..<(bytes.count - 32)]) let agreement = secondaryDeviceKeyPair.privateKey.keyAgreement( with: primaryDeviceEphemeralPublicKey) let keyBytes = try Self.messageInfo.utf8.withContiguousStorageIfAvailable { try hkdf(outputLength: 64, inputKeyMaterial: agreement, salt: [], info: $0) }! let cipherKey = Array(keyBytes[0..<32]) let macKey = keyBytes[32..<64] guard let ourHMAC = Cryptography.computeSHA256HMAC(Data(messageToAuthenticate), key: Data(macKey)) else { throw OWSAssertionError("ourHMAC was unexpectedly nil") } guard ourHMAC.ows_constantTimeIsEqual(to: Data(theirMac)) else { throw ProvisioningError.invalidProvisionMessage("mac mismatch") } var bytesDecrypted: size_t = 0 var plaintextBuffer: [UInt8] = [UInt8].init(repeating: 0, count: ciphertext.count + kCCBlockSizeAES128) let cryptStatus = CCCrypt(CCOperation(kCCDecrypt), CCAlgorithm(kCCAlgorithmAES128), CCOptions(kCCOptionPKCS7Padding), cipherKey, cipherKey.count, iv, ciphertext, ciphertext.count, &plaintextBuffer, plaintextBuffer.count, &bytesDecrypted) guard cryptStatus == kCCSuccess else { throw OWSAssertionError("failure with cryptStatus: \(cryptStatus)") } let plaintext = Data(plaintextBuffer.prefix(upTo: bytesDecrypted)) let proto = try ProvisioningProtoProvisionMessage(serializedData: plaintext) let aciIdentityKeyPair = try IdentityKeyPair(publicKey: PublicKey(proto.aciIdentityKeyPublic), privateKey: PrivateKey(proto.aciIdentityKeyPrivate)) let pniIdentityKeyPair = try IdentityKeyPair(publicKey: PublicKey(proto.pniIdentityKeyPublic), privateKey: PrivateKey(proto.pniIdentityKeyPrivate)) guard let profileKey = OWSAES256Key(data: proto.profileKey) else { throw ProvisioningError.invalidProvisionMessage("invalid profileKey - count: \(proto.profileKey.count)") } let areReadReceiptsEnabled = proto.hasReadReceipts ? proto.readReceipts : nil let primaryUserAgent = proto.hasUserAgent ? proto.userAgent : nil let provisioningCode = proto.provisioningCode let provisioningVersion = proto.hasProvisioningVersion ? proto.provisioningVersion : nil guard let phoneNumber = proto.number, phoneNumber.count > 1 else { throw ProvisioningError.invalidProvisionMessage("missing number from provisioning message") } let aci: Aci? = try { guard proto.hasAci, let aciString = proto.aci else { return nil } guard let aci = Aci.parseFrom(aciString: aciString) else { throw ProvisioningError.invalidProvisionMessage("invalid ACI from provisioning message") } return aci }() let pni: Pni? = try { guard proto.hasPni, let pniString = proto.pni else { return nil } guard let pni = Pni.parseFrom(ambiguousString: pniString) else { throw ProvisioningError.invalidProvisionMessage("invalid PNI from provisioning message") } return pni }() guard let masterKey = proto.masterKey else { throw ProvisioningError.invalidProvisionMessage("missing master key from provisioning message") } return ProvisionMessage( aci: aci, phoneNumber: phoneNumber, pni: pni, aciIdentityKeyPair: ECKeyPair(aciIdentityKeyPair), pniIdentityKeyPair: ECKeyPair(pniIdentityKeyPair), profileKey: profileKey, masterKey: masterKey, areReadReceiptsEnabled: areReadReceiptsEnabled, primaryUserAgent: primaryUserAgent, provisioningCode: provisioningCode, provisioningVersion: provisioningVersion ) } }