107 lines
3.8 KiB
Swift
107 lines
3.8 KiB
Swift
//
|
|
// Copyright 2019 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
public import CommonCrypto
|
|
import CryptoKit
|
|
public import LibSignalClient
|
|
|
|
public enum ProvisioningError: Error {
|
|
case invalidProvisionMessage(_ description: String)
|
|
}
|
|
|
|
public class ProvisioningCipher {
|
|
|
|
private enum Constants {
|
|
static let version: UInt8 = 1
|
|
static let cipherKeyLength: Int = 32
|
|
static let macKeyLength: Int = 32
|
|
static let info: String = "TextSecure Provisioning Message"
|
|
}
|
|
|
|
private let ourKeyPair: IdentityKeyPair
|
|
|
|
public var ourPublicKey: PublicKey {
|
|
return ourKeyPair.publicKey
|
|
}
|
|
|
|
public init(ourKeyPair: IdentityKeyPair) {
|
|
self.ourKeyPair = ourKeyPair
|
|
}
|
|
|
|
public func encrypt(
|
|
_ data: Data,
|
|
initializationVector: Data = Randomness.generateRandomBytes(UInt(kCCBlockSizeAES128)),
|
|
theirPublicKey: PublicKey,
|
|
) throws -> Data {
|
|
let sharedSecret = self.ourKeyPair.privateKey.keyAgreement(with: theirPublicKey)
|
|
|
|
let infoData = Constants.info
|
|
let totalLength = Constants.cipherKeyLength + Constants.macKeyLength
|
|
let derivedSecret = try hkdf(outputLength: totalLength, inputKeyMaterial: sharedSecret, salt: [], info: Data(infoData.utf8))
|
|
owsPrecondition(derivedSecret.count == totalLength)
|
|
let cipherKey = derivedSecret.prefix(Constants.cipherKeyLength)
|
|
let macKey = derivedSecret.dropFirst(Constants.cipherKeyLength)
|
|
owsAssertDebug(macKey.count == Constants.macKeyLength)
|
|
|
|
guard data.count < Int.max - (kCCBlockSizeAES128 + initializationVector.count) else {
|
|
throw ProvisioningError.invalidProvisionMessage("data too long to encrypt.")
|
|
}
|
|
|
|
let ciphertextData = try Cryptography.encrypt(plaintextData: data, key: cipherKey, iv: initializationVector)
|
|
|
|
var message = Data()
|
|
let version: UInt8 = 1
|
|
message.append(version)
|
|
// message format is (iv || ciphertext)
|
|
message.append(initializationVector)
|
|
message.append(ciphertextData)
|
|
message.append(contentsOf: HMAC<SHA256>.authenticationCode(for: message, using: .init(data: macKey)))
|
|
return message
|
|
}
|
|
|
|
public func decrypt(data bytes: Data, theirPublicKey: PublicKey) throws -> Data {
|
|
var bytes = bytes
|
|
|
|
let versionLength = 1
|
|
let ivLength = 16
|
|
let macLength = 32
|
|
|
|
let theirMac = bytes.suffix(macLength)
|
|
bytes = bytes.dropLast(macLength)
|
|
|
|
let messageToAuthenticate = bytes
|
|
|
|
let version = bytes.first
|
|
bytes = bytes.dropFirst(versionLength)
|
|
|
|
let initializationVector = bytes.prefix(ivLength)
|
|
bytes = bytes.dropFirst(ivLength)
|
|
|
|
let ciphertext = bytes
|
|
|
|
guard let version, initializationVector.count == ivLength, theirMac.count == macLength, !ciphertext.isEmpty else {
|
|
throw ProvisioningError.invalidProvisionMessage("provisioning message too short.")
|
|
}
|
|
|
|
guard version == Constants.version else {
|
|
throw ProvisioningError.invalidProvisionMessage("Unexpected version on provisioning message: \(version)")
|
|
}
|
|
|
|
let agreement = ourKeyPair.privateKey.keyAgreement(with: theirPublicKey)
|
|
|
|
let keyBytes = try hkdf(outputLength: 64, inputKeyMaterial: agreement, salt: [], info: Data(Constants.info.utf8))
|
|
owsPrecondition(keyBytes.count == 64)
|
|
let cipherKey = keyBytes.prefix(32)
|
|
let macKey = keyBytes.dropFirst(32).prefix(32)
|
|
|
|
let ourHMAC = Data(HMAC<SHA256>.authenticationCode(for: messageToAuthenticate, using: .init(data: macKey)))
|
|
guard ourHMAC.ows_constantTimeIsEqual(to: theirMac) else {
|
|
throw ProvisioningError.invalidProvisionMessage("mac mismatch")
|
|
}
|
|
|
|
return try Cryptography.decrypt(encryptedData: ciphertext, key: cipherKey, iv: initializationVector)
|
|
}
|
|
}
|