mirror of
https://github.com/AU-COVIDSafe/mobile-ios.git
synced 2025-04-19 13:05:21 +00:00
COVIDSafe code from version 1.4 (#3)
This commit is contained in:
parent
56c93f2079
commit
b2e0c5b34c
33 changed files with 6865 additions and 481 deletions
196
CovidSafe/Crypto.swift
Normal file
196
CovidSafe/Crypto.swift
Normal file
|
@ -0,0 +1,196 @@
|
|||
//
|
||||
// Crypto.swift
|
||||
// CovidSafe
|
||||
//
|
||||
// Copyright © 2020 Australian Government. All rights reserved.
|
||||
//
|
||||
|
||||
import CommonCrypto
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
enum SecurityError: Error {
|
||||
case PublicKeyCopyError
|
||||
case EncryptionFailedError(_ status: CCCryptorStatus)
|
||||
case DigestFailedError
|
||||
case EncryptionLengthError
|
||||
case EncryptionKeyLengthError
|
||||
case UnexpectedNilKeys
|
||||
}
|
||||
|
||||
|
||||
class Crypto {
|
||||
private static var cachedExportPublicKey: Data?
|
||||
private static var cachedAesKey: Data?
|
||||
private static var cachedMacKey: Data?
|
||||
private static var keyGenTime: Int64 = Int64.min
|
||||
private static var counter: UInt16 = 0
|
||||
private static let NONCE_PADDING: Data = Data([UInt8](repeating: UInt8(0x0E), count: 14))
|
||||
private static let keyCacheQueue = DispatchQueue(label: "au.gov.health.covidsafe.crypto")
|
||||
private static let KEY_GEN_TIME_DELTA: Int64 = 450 // 7.5 minutes
|
||||
#if DEBUG
|
||||
private static let publicKey = Data(base64Encoded: "BNrAcR+C6nkCpIYS9KWYt0Z5Sbleh7UybHmIT2T9YzuR9RzTh3YZcMBjr1K6smeDJW7sPCvMFJNWVPkk3exqjkQ=")
|
||||
#else
|
||||
private static let publicKey = Data(base64Encoded: "BDQbOM4lxeK6ed9br26qvcwsYgaUK9w3CozIHP1gOhR7+qwb7vrh0kSSUUtsayekard9EHElA9RNn/3dJW9hr7I=")
|
||||
#endif
|
||||
|
||||
/**
|
||||
Get a series of secrets that can be decrypted by the server key. The returned data is:
|
||||
1. the ephemeral public key used for decrypting
|
||||
2. the AES encryption key
|
||||
3. the HMAC signature key
|
||||
4. the IV for AES encryption
|
||||
- Parameter serverKey: X9.63 formatted P-256 public key for the server
|
||||
- Throws: Errors from Security framework, or `SecurityError.PublicKeyCopyError`
|
||||
if function failed to derive public key from the ephemeral private key
|
||||
- Returns:
|
||||
- publicKey: exported public P-256 key (compressed form)
|
||||
- aesKey: ephemeral 16-byte AES-128 key
|
||||
- macKey: ephemeral 16-byte key for HMAC
|
||||
- iv: ephemeral 16-byte AES-128 IV
|
||||
*/
|
||||
private static func getEphemeralSecrets(_ serverKey: Data) throws -> (publicKey: Data, aesKey: Data, macKey: Data) {
|
||||
// Server public key
|
||||
var err: Unmanaged<CFError>?
|
||||
let serverKeyOptions: [CFString: Any] = [
|
||||
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
|
||||
kSecAttrKeySizeInBits: 256,
|
||||
kSecAttrKeyClass: kSecAttrKeyClassPublic,
|
||||
]
|
||||
guard let serverPublicKey = SecKeyCreateWithData(serverKey as CFData, serverKeyOptions as CFDictionary, &err) else {
|
||||
throw err!.takeRetainedValue() as Error
|
||||
}
|
||||
|
||||
// CREATE A LOCAL EPHEMERAL P-256 KEYPAIR
|
||||
let ephereralPublicKeyAttributes: [CFString: Any] = [
|
||||
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
|
||||
kSecAttrKeySizeInBits: 256,
|
||||
]
|
||||
|
||||
guard let ephemeralPrivateKey = SecKeyCreateRandomKey(ephereralPublicKeyAttributes as CFDictionary, &err) else {
|
||||
throw err!.takeRetainedValue() as Error
|
||||
}
|
||||
guard let ephemeralPublicKey = SecKeyCopyPublicKey(ephemeralPrivateKey) else {
|
||||
throw SecurityError.PublicKeyCopyError
|
||||
}
|
||||
|
||||
// Exported ephemeral public key for sending/MACing later (compressed format, per ANSI X9.62)
|
||||
let exportPublicKey = try ephemeralPublicKey.CopyCompressedECPublicKey()
|
||||
|
||||
// COMPUTE SHARED SECRET
|
||||
let params = [SecKeyKeyExchangeParameter.requestedSize.rawValue: 32]
|
||||
guard let sharedSecret = SecKeyCopyKeyExchangeResult(ephemeralPrivateKey,
|
||||
SecKeyAlgorithm.ecdhKeyExchangeStandard,
|
||||
serverPublicKey,
|
||||
params as CFDictionary,
|
||||
&err) as Data? else {
|
||||
throw err!.takeRetainedValue() as Error
|
||||
}
|
||||
|
||||
// KDF THE SHARED SECRET TO GET ENC KEY, MAC KEY
|
||||
var keysHashCtx = CC_SHA256_CTX()
|
||||
|
||||
// For keys we'll be using SHA256(sharedSecret)
|
||||
var res: Int32
|
||||
var keysHashValue = Data(count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
CC_SHA256_Init(&keysHashCtx)
|
||||
res = sharedSecret.withUnsafeBytes {
|
||||
return CC_SHA256_Update(&keysHashCtx, $0.baseAddress, CC_LONG(sharedSecret.count))
|
||||
}
|
||||
guard res == 1 else { throw SecurityError.DigestFailedError }
|
||||
res = keysHashValue.withUnsafeMutableBytes {
|
||||
return CC_SHA256_Final($0.bindMemory(to: UInt8.self).baseAddress, &keysHashCtx)
|
||||
}
|
||||
guard res == 1 else { throw SecurityError.DigestFailedError }
|
||||
|
||||
// Form the keys
|
||||
let aesKey = keysHashValue[..<kCCKeySizeAES128]
|
||||
let macKey = keysHashValue[kCCKeySizeAES128...]
|
||||
|
||||
// At return, the refs to ephemeralPrivateKey and sharedSecret will be dropped and they will be cleared
|
||||
return (exportPublicKey, aesKey, macKey)
|
||||
}
|
||||
|
||||
static func buildSecretData(_ serverPublicKey: Data, _ plaintext: Data) throws -> Data {
|
||||
// Get our ephemeral secrets that will de disposed at the end of this function
|
||||
let (cachedExportPublicKey, cachedAESKey, cachedMacKey, nonce) = try keyCacheQueue.sync { () -> (Data?, Data?, Data?, Data) in
|
||||
if Crypto.keyGenTime <= Int64(Date().timeIntervalSince1970) - KEY_GEN_TIME_DELTA || Crypto.counter >= 65535 {
|
||||
(Crypto.cachedExportPublicKey, Crypto.cachedAesKey, Crypto.cachedMacKey) = try getEphemeralSecrets(serverPublicKey)
|
||||
Crypto.keyGenTime = Int64(Date().timeIntervalSince1970)
|
||||
Crypto.counter = 0
|
||||
} else {
|
||||
Crypto.counter += 1
|
||||
}
|
||||
let nonce = withUnsafeBytes(of: Crypto.counter.bigEndian) { Data($0) }
|
||||
return (Crypto.cachedExportPublicKey, Crypto.cachedAesKey, Crypto.cachedMacKey, nonce)
|
||||
}
|
||||
guard let exportPublicKey = cachedExportPublicKey, let aesKey = cachedAESKey, let macKey = cachedMacKey else {
|
||||
throw SecurityError.UnexpectedNilKeys
|
||||
}
|
||||
|
||||
|
||||
// AES ENCRYPT DATA
|
||||
// IV = AES(ctr, iv=null), AES(plaintext, iv=IV) === AES(ctr_with_padding || plaintext, iv=null)
|
||||
// Using the latter construction to reduce key expansions
|
||||
|
||||
// Under PKCS#7 padding, we pad out to a complete blocksize but if the input is an exact multiple of blocksize,
|
||||
// then we add an extra block on. So in both cases it's 16 bytes + (dataLen/16 + 1) * 16 bytes long
|
||||
let outputLen = ((plaintext.count / kCCBlockSizeAES128) + 2) * kCCBlockSizeAES128
|
||||
|
||||
let nullIV = Data(count: 16)
|
||||
var plaintextWithIV = Data(capacity: plaintext.count + 16)
|
||||
plaintextWithIV.append(nonce)
|
||||
plaintextWithIV.append(NONCE_PADDING)
|
||||
plaintextWithIV.append(plaintext)
|
||||
|
||||
var ciphertextWithIV = Data(count: outputLen)
|
||||
var dataWrittenLen = 0
|
||||
let status = ciphertextWithIV.withUnsafeMutableBytes { ciphertextPtr in
|
||||
plaintextWithIV.withUnsafeBytes { plaintextPtr in
|
||||
nullIV.withUnsafeBytes { ivPtr in
|
||||
aesKey.withUnsafeBytes { aesKeyPtr in
|
||||
return CCCrypt(CCOperation(kCCEncrypt), CCAlgorithm(kCCAlgorithmAES), CCOptions(kCCOptionPKCS7Padding),
|
||||
aesKeyPtr.baseAddress, kCCKeySizeAES128, ivPtr.baseAddress,
|
||||
plaintextPtr.baseAddress, plaintextWithIV.count,
|
||||
ciphertextPtr.baseAddress, outputLen,
|
||||
&dataWrittenLen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
guard status == kCCSuccess else {
|
||||
throw SecurityError.EncryptionFailedError(status)
|
||||
}
|
||||
guard outputLen == dataWrittenLen else {
|
||||
throw SecurityError.EncryptionLengthError
|
||||
}
|
||||
|
||||
let ciphertext = ciphertextWithIV[16...]
|
||||
|
||||
// HMAC
|
||||
var macValue = Data(count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
var hmacContext = CCHmacContext()
|
||||
macKey.withUnsafeBytes { CCHmacInit(&hmacContext, CCHmacAlgorithm(kCCHmacAlgSHA256), $0.baseAddress, macKey.count) }
|
||||
exportPublicKey.withUnsafeBytes { CCHmacUpdate(&hmacContext, $0.baseAddress, exportPublicKey.count) }
|
||||
nonce.withUnsafeBytes { CCHmacUpdate(&hmacContext, $0.baseAddress, nonce.count) }
|
||||
ciphertext.withUnsafeBytes { CCHmacUpdate(&hmacContext, $0.baseAddress, ciphertext.count) }
|
||||
macValue.withUnsafeMutableBytes { CCHmacFinal(&hmacContext, $0.bindMemory(to: UInt8.self).baseAddress) }
|
||||
|
||||
// Build the final payload: ephemeral public key || nonce || encrypted data || HMAC
|
||||
var finalData = Data(capacity: exportPublicKey.count + ciphertext.count + 18)
|
||||
finalData.append(exportPublicKey)
|
||||
finalData.append(nonce)
|
||||
finalData.append(ciphertext)
|
||||
finalData.append(macValue[..<16])
|
||||
|
||||
return finalData
|
||||
}
|
||||
|
||||
public static func encrypt(dataToEncrypt: Data) throws -> String {
|
||||
guard let publicKey = publicKey else {
|
||||
throw SecurityError.PublicKeyCopyError
|
||||
}
|
||||
let encryptedData = try buildSecretData(publicKey, dataToEncrypt)
|
||||
return encryptedData.base64EncodedString()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue