sdk/communication/AzureCommunicationChat/Source/NotificationUtils/CryptoUtils.swift (123 lines of code) (raw):
// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the ""Software""), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//
// --------------------------------------------------------------------------
import AzureCore
import CommonCrypto
import CryptoKit
import Foundation
class CryptoUtils {
static let ciperModeSize: Int = 1
static let initializationVectorSize: Int = 16
static let hmacSize: Int = 32
static func extractInitializationVector(result: [UInt8]) -> [UInt8] {
return copyOfRange(
originalArr: result,
startIdx: ciperModeSize,
endIdx: ciperModeSize + initializationVectorSize
)
}
static func extractCipherText(result: [UInt8]) -> [UInt8] {
return copyOfRange(
originalArr: result,
startIdx: ciperModeSize + initializationVectorSize,
endIdx: ciperModeSize + initializationVectorSize +
(result.count - hmacSize - ciperModeSize - initializationVectorSize)
)
}
static func extractHmac(result: [UInt8]) -> [UInt8] {
let testArr1 = copyOfRange(originalArr: result, startIdx: result.count - hmacSize, endIdx: result.count)
print("Print hmac:", String(decoding: testArr1, as: UTF8.self))
return testArr1
}
static func extractCipherModeIVCipherText(result: [UInt8]) -> [UInt8] {
let testArr = copyOfRange(originalArr: result, startIdx: 0, endIdx: result.count - hmacSize)
print("Print cipherModeIVText:", String(decoding: testArr, as: UTF8.self))
return testArr
}
}
func copyOfRange(originalArr: [UInt8], startIdx: Int, endIdx: Int) -> [UInt8] {
var arrCopy = [UInt8](repeating: 0, count: endIdx - startIdx)
for idx in startIdx ..< endIdx {
arrCopy[idx - startIdx] = originalArr[idx]
}
return arrCopy
}
// Verify HMAC SHA256 signature using CryptoKit Framework
func verifyEncryptedPayload(cipherModeIVCipherText: [UInt8], authKey: String, actualHmac: [UInt8]) throws -> Bool {
// 1.Calculate SHA256 key
guard let data = Data(base64Encoded: authKey) else {
throw AzureError
.client(
"Failed to initialize a data object with the given authKey. Please ensure the authKey is a Base64 encoded string."
)
}
let digest = SHA256.hash(data: data)
let key = SymmetricKey(data: digest.data)
// 2.Computed HMAC signature
let signature = HMAC<SHA256>.authenticationCode(for: Data(cipherModeIVCipherText), using: key)
let calculatedHMACHex = Data(signature).map { String(format: "%02hhx", $0) }.joined()
print("calculatedMac:\(calculatedHMACHex)")
// 3.Included HMAC signature
let actualHMACHex = Data(actualHmac).map { String(format: "%02hhx", $0) }.joined()
print("actualHmac:\(actualHMACHex)")
return actualHMACHex == calculatedHMACHex
}
extension Digest {
var bytes: [UInt8] { Array(makeIterator()) }
var data: Data { Data(bytes) }
}
// Decrypt the notification payload using CommonCrypto Framework
func decryptPushNotificationPayload(cipherText: [UInt8], iv: [UInt8], cryptoKey: String) throws -> String {
guard let decodedData = Data(base64Encoded: cryptoKey) else {
throw AzureError
.client(
"Failed to initialize a data object with the given cryptoKey. Please ensure the cryptoKey is a Base64 encoded string."
)
}
let keyBytes = Array(decodedData)
let cryptLength = size_t(cipherText.count + kCCBlockSizeAES128)
var cryptData = [UInt8](repeating: 0, count: cryptLength)
let keyLength = size_t(kCCKeySizeAES256)
let algoritm: CCAlgorithm = UInt32(kCCAlgorithmAES)
let options: CCOptions = UInt32(kCCOptionPKCS7Padding)
var numBytesEncrypted: size_t = 0
let cryptStatus = CCCrypt(
CCOperation(kCCDecrypt),
algoritm,
options,
keyBytes,
keyLength,
iv,
cipherText,
cipherText.count,
&cryptData,
cryptLength,
&numBytesEncrypted
)
if UInt32(cryptStatus) == UInt32(kCCSuccess) {
cryptData.removeSubrange(numBytesEncrypted ..< cryptData.count)
} else {
throw AzureError.client("Error in decrypting Notification Payload: \(cryptStatus)")
}
return String(decoding: cryptData, as: UTF8.self)
}
internal func generateEncryptionKey() -> String {
return SymmetricKey(size: .init(bitCount: 512)).serialize()
}
// Use the SymmetricKey class in CryptoKit framework to create encryption keys
extension SymmetricKey {
/// Serializes a `SymmetricKey` to a Base64-encoded `String`.
func serialize() -> String {
return withUnsafeBytes { body in
Data(body).base64EncodedString()
}
}
}
func splitEncryptionKey(encryptionKey: String) throws -> [String] {
guard var data = Data(base64Encoded: encryptionKey, options: .ignoreUnknownCharacters) else {
throw AzureError
.client("Failed to convert base64Encoded string into Data format.")
}
var aesArr: [UInt8] = .init(repeating: 0, count: 32)
var authArr: [UInt8] = .init(repeating: 0, count: 32)
for idx in 0 ..< 64 {
if idx < 32 {
aesArr[idx] = data.remove(at: 0)
} else {
authArr[idx - 32] = data.remove(at: 0)
}
}
let aesKey = Data(aesArr).base64EncodedString()
let authKey = Data(authArr).base64EncodedString()
return [aesKey, authKey]
}