Add streaming encryption and decryption APIs

This commit is contained in:
Nora Trapp 2021-04-06 15:20:13 -07:00
parent 442e2d544a
commit 64410ab6b3
6 changed files with 683 additions and 657 deletions

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@ -75,40 +75,8 @@ typedef NS_ENUM(NSInteger, TSMACType) {
#pragma mark - SHA and HMAC methods
// Full length SHA256 digest for `data`
+ (nullable NSData *)computeSHA256Digest:(NSData *)data;
// Truncated SHA256 digest for `data`
+ (nullable NSData *)computeSHA256Digest:(NSData *)data truncatedToBytes:(NSUInteger)truncatedBytes;
+ (nullable NSString *)truncatedSHA1Base64EncodedWithoutPadding:(NSString *)string;
+ (nullable NSData *)decryptAppleMessagePayload:(NSData *)payload withSignalingKey:(NSString *)signalingKeyString;
+ (nullable NSData *)computeSHA256HMAC:(NSData *)data withHMACKey:(NSData *)HMACKey;
+ (nullable NSData *)truncatedSHA256HMAC:(NSData *)dataToHMAC
withHMACKey:(NSData *)HMACKey
truncation:(NSUInteger)truncation;
#pragma mark - Attachments & Stickers
// Though digest can and will be nil for legacy clients, we now reject attachments lacking a digest.
+ (nullable NSData *)decryptAttachment:(NSData *)dataToDecrypt
withKey:(NSData *)key
digest:(nullable NSData *)digest
unpaddedSize:(UInt32)unpaddedSize
error:(NSError **)error;
+ (nullable NSData *)decryptStickerData:(NSData *)dataToDecrypt
withKey:(NSData *)key
error:(NSError **)error;
+ (nullable NSData *)encryptAttachmentData:(NSData *)attachmentData
shouldPad:(BOOL)shouldPad
outKey:(NSData *_Nonnull *_Nullable)outKey
outDigest:(NSData *_Nonnull *_Nullable)outDigest;
#pragma mark - AES-GCM
+ (nullable AES25GCMEncryptionResult *)encryptAESGCMWithData:(NSData *)plaintext
@ -164,8 +132,6 @@ typedef NS_ENUM(NSInteger, TSMACType) {
+ (void)seedRandom;
+ (unsigned long)paddedSize:(unsigned long)unpaddedSize NS_SWIFT_NAME(paddedSize(unpaddedSize:));
@end
NS_ASSUME_NONNULL_END

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import "Cryptography.h"
@ -12,11 +12,6 @@
NS_ASSUME_NONNULL_BEGIN
#define HMAC256_KEY_LENGTH 32
#define HMAC256_OUTPUT_LENGTH 32
#define AES_CBC_IV_LENGTH 16
#define AES_KEY_SIZE 32
// Returned by many OpenSSL functions - indicating success
const int kOpenSSLSuccess = 1;
@ -233,458 +228,6 @@ const NSUInteger kAES256_KeyByteLength = 32;
return [[truncatedData base64EncodedString] stringByReplacingOccurrencesOfString:@"=" withString:@""];
}
#pragma mark - SHA256 Digest
+ (nullable NSData *)computeSHA256Digest:(NSData *)data
{
return [self computeSHA256Digest:data truncatedToBytes:CC_SHA256_DIGEST_LENGTH];
}
+ (nullable NSData *)computeSHA256Digest:(NSData *)data truncatedToBytes:(NSUInteger)truncatedBytes
{
if (data.length >= UINT32_MAX) {
OWSFailDebug(@"data is too long.");
return nil;
}
uint32_t dataLength = (uint32_t)data.length;
NSMutableData *_Nullable digestData = [[NSMutableData alloc] initWithLength:CC_SHA256_DIGEST_LENGTH];
if (!digestData) {
OWSFailDebug(@"could not allocate buffer.");
return nil;
}
CC_SHA256(data.bytes, dataLength, digestData.mutableBytes);
return [digestData subdataWithRange:NSMakeRange(0, truncatedBytes)];
}
#pragma mark - HMAC/SHA256
+ (nullable NSData *)computeSHA256HMAC:(NSData *)data withHMACKey:(NSData *)HMACKey
{
if (data.length >= SIZE_MAX) {
OWSFailDebug(@"data is too long.");
return nil;
}
size_t dataLength = (size_t)data.length;
if (HMACKey.length >= SIZE_MAX) {
OWSFailDebug(@"HMAC key is too long.");
return nil;
}
size_t hmacKeyLength = (size_t)HMACKey.length;
NSMutableData *_Nullable ourHmacData = [[NSMutableData alloc] initWithLength:CC_SHA256_DIGEST_LENGTH];
if (!ourHmacData) {
OWSFailDebug(@"could not allocate buffer.");
return nil;
}
CCHmac(kCCHmacAlgSHA256, [HMACKey bytes], hmacKeyLength, [data bytes], dataLength, ourHmacData.mutableBytes);
return [ourHmacData copy];
}
+ (nullable NSData *)truncatedSHA256HMAC:(NSData *)dataToHMAC
withHMACKey:(NSData *)HMACKey
truncation:(NSUInteger)truncation
{
OWSAssert(truncation <= CC_SHA256_DIGEST_LENGTH);
OWSAssert(dataToHMAC);
OWSAssert(HMACKey);
return
[[Cryptography computeSHA256HMAC:dataToHMAC withHMACKey:HMACKey] subdataWithRange:NSMakeRange(0, truncation)];
}
#pragma mark - AES CBC Mode
/**
* AES256 CBC encrypt then mac. Used to decrypt both signal messages and attachment blobs
*
* @return decrypted data or nil if hmac invalid/decryption fails
*/
+ (nullable NSData *)decryptCBCMode:(NSData *)dataToDecrypt
key:(NSData *)key
IV:(NSData *)iv
version:(nullable NSData *)version
HMACKey:(NSData *)hmacKey
HMACType:(TSMACType)hmacType
matchingHMAC:(NSData *)hmac
digest:(nullable NSData *)digest
{
OWSAssert(dataToDecrypt);
OWSAssert(key);
if (key.length != kCCKeySizeAES256) {
OWSFailDebug(@"key had wrong size.");
return nil;
}
OWSAssert(iv);
if (iv.length != kCCBlockSizeAES128) {
OWSFailDebug(@"iv had wrong size.");
return nil;
}
OWSAssert(hmacKey);
OWSAssert(hmac);
size_t bufferSize;
BOOL didOverflow = __builtin_add_overflow(dataToDecrypt.length, kCCBlockSizeAES128, &bufferSize);
if (didOverflow) {
OWSFailDebug(@"bufferSize was too large.");
return nil;
}
// Verify hmac of: version? || iv || encrypted data
NSUInteger dataToAuthLength = 0;
if (__builtin_add_overflow(dataToDecrypt.length, iv.length, &dataToAuthLength)) {
OWSFailDebug(@"dataToAuth was too large.");
return nil;
}
if (version != nil && __builtin_add_overflow(dataToAuthLength, version.length, &dataToAuthLength)) {
OWSFailDebug(@"dataToAuth was too large.");
return nil;
}
NSMutableData *dataToAuth = [NSMutableData data];
if (version != nil) {
[dataToAuth appendData:version];
}
[dataToAuth appendData:iv];
[dataToAuth appendData:dataToDecrypt];
NSData *_Nullable ourHmacData;
if (hmacType == TSHMACSHA256Truncated10Bytes) {
// used to authenticate envelope from websocket
OWSAssert(hmacKey.length == kHMAC256_EnvelopeKeyLength);
ourHmacData = [Cryptography truncatedSHA256HMAC:dataToAuth withHMACKey:hmacKey truncation:10];
OWSAssert(ourHmacData.length == 10);
} else if (hmacType == TSHMACSHA256AttachementType) {
OWSAssert(hmacKey.length == HMAC256_KEY_LENGTH);
ourHmacData =
[Cryptography truncatedSHA256HMAC:dataToAuth withHMACKey:hmacKey truncation:HMAC256_OUTPUT_LENGTH];
OWSAssert(ourHmacData.length == HMAC256_OUTPUT_LENGTH);
} else {
OWSFail(@"unknown HMAC scheme: %ld", (long)hmacType);
}
if (hmac == nil || ![ourHmacData ows_constantTimeIsEqualToData:hmac]) {
OWSLogError(@"Bad HMAC on decrypting payload.");
// Don't log HMAC in prod
OWSLogDebug(@"Bad HMAC on decrypting payload. Their MAC: %@, our MAC: %@", hmac, ourHmacData);
return nil;
}
// Optionally verify digest of: version? || iv || encrypted data || hmac
if (digest) {
OWSLogDebug(@"verifying their digest");
[dataToAuth appendData:ourHmacData];
NSData *_Nullable ourDigest = [Cryptography computeSHA256Digest:dataToAuth];
if (!ourDigest || ![ourDigest ows_constantTimeIsEqualToData:digest]) {
OWSLogWarn(@"Bad digest on decrypting payload");
// Don't log digest in prod
DDLogDebug(@"Bad digest on decrypting payload. Their digest: %@, our digest: %@, data: %@",
digest.hexadecimalString,
ourDigest.hexadecimalString,
dataToAuth.hexadecimalString);
return nil;
}
}
// decrypt
NSMutableData *_Nullable bufferData = [NSMutableData dataWithLength:bufferSize];
if (!bufferData) {
OWSLogError(@"Failed to allocate buffer.");
return nil;
}
size_t bytesDecrypted = 0;
CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt,
kCCAlgorithmAES128,
kCCOptionPKCS7Padding,
[key bytes],
[key length],
[iv bytes],
[dataToDecrypt bytes],
[dataToDecrypt length],
bufferData.mutableBytes,
bufferSize,
&bytesDecrypted);
if (cryptStatus == kCCSuccess) {
return [bufferData subdataWithRange:NSMakeRange(0, bytesDecrypted)];
} else {
OWSLogError(@"Failed CBC decryption");
}
return nil;
}
#pragma mark - methods which use AES CBC
+ (nullable NSData *)decryptAppleMessagePayload:(NSData *)payload withSignalingKey:(NSString *)signalingKeyString
{
OWSAssertDebug(payload);
OWSAssertDebug(signalingKeyString);
size_t versionLength = 1;
size_t ivLength = 16;
size_t macLength = 10;
size_t nonCiphertextLength = versionLength + ivLength + macLength;
size_t ciphertextLength;
ows_sub_overflow(payload.length, nonCiphertextLength, &ciphertextLength);
if (payload.length < nonCiphertextLength) {
OWSFailDebug(@"Invalid payload");
return nil;
}
if (payload.length >= MIN(SIZE_MAX, NSUIntegerMax) - nonCiphertextLength) {
OWSFailDebug(@"Invalid payload");
return nil;
}
NSUInteger cursor = 0;
NSData *versionData = [payload subdataWithRange:NSMakeRange(cursor, versionLength)];
cursor += versionLength;
NSData *ivData = [payload subdataWithRange:NSMakeRange(cursor, ivLength)];
cursor += ivLength;
NSData *ciphertextData = [payload subdataWithRange:NSMakeRange(cursor, ciphertextLength)];
ows_add_overflow(cursor, ciphertextLength, &cursor);
NSData *macData = [payload subdataWithRange:NSMakeRange(cursor, macLength)];
NSData *signalingKey = [NSData dataFromBase64String:signalingKeyString];
NSData *signalingKeyAESKeyMaterial = [signalingKey subdataWithRange:NSMakeRange(0, 32)];
NSData *signalingKeyHMACKeyMaterial = [signalingKey subdataWithRange:NSMakeRange(32, kHMAC256_EnvelopeKeyLength)];
return [Cryptography decryptCBCMode:ciphertextData
key:signalingKeyAESKeyMaterial
IV:ivData
version:versionData
HMACKey:signalingKeyHMACKeyMaterial
HMACType:TSHMACSHA256Truncated10Bytes
matchingHMAC:macData
digest:nil];
}
#pragma mark - Attachments & Stickers
+ (nullable NSData *)decryptAttachment:(NSData *)dataToDecrypt
withKey:(NSData *)key
digest:(nullable NSData *)digest
unpaddedSize:(UInt32)unpaddedSize
error:(NSError **)error
{
if (digest.length <= 0) {
// This *could* happen with sufficiently outdated clients.
OWSLogError(@"Refusing to decrypt attachment without a digest.");
*error = SCKErrorWithCodeDescription(SCKErrorCode_FailedToDecryptMessage,
NSLocalizedString(@"ERROR_MESSAGE_ATTACHMENT_FROM_OLD_CLIENT",
@"Error message when unable to receive an attachment because the sending client is too old."));
return nil;
}
return [self decryptData:dataToDecrypt
withKey:key
digest:digest
unpaddedSize:unpaddedSize
error:error];
}
+ (nullable NSData *)decryptStickerData:(NSData *)dataToDecrypt
withKey:(NSData *)key
error:(NSError **)error
{
return [self decryptData:dataToDecrypt
withKey:key
digest:nil
unpaddedSize:0
error:error];
}
+ (nullable NSData *)decryptData:(NSData *)dataToDecrypt
withKey:(NSData *)key
digest:(nullable NSData *)digest
unpaddedSize:(UInt32)unpaddedSize
error:(NSError **)error
{
if (([dataToDecrypt length] < AES_CBC_IV_LENGTH + HMAC256_OUTPUT_LENGTH) ||
([key length] < AES_KEY_SIZE + HMAC256_KEY_LENGTH)) {
OWSLogError(@"Message shorter than crypto overhead!");
*error = SCKErrorWithCodeDescription(SCKErrorCode_FailedToDecryptMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @""));
return nil;
}
// key: 32 byte AES key || 32 byte Hmac-SHA256 key.
NSData *encryptionKey = [key subdataWithRange:NSMakeRange(0, AES_KEY_SIZE)];
NSData *hmacKey = [key subdataWithRange:NSMakeRange(AES_KEY_SIZE, HMAC256_KEY_LENGTH)];
// dataToDecrypt: IV || Ciphertext || truncated MAC(IV||Ciphertext)
NSData *iv = [dataToDecrypt subdataWithRange:NSMakeRange(0, AES_CBC_IV_LENGTH)];
NSUInteger cipherTextLength;
ows_sub_overflow(dataToDecrypt.length, (AES_CBC_IV_LENGTH + HMAC256_OUTPUT_LENGTH), &cipherTextLength);
NSData *encryptedAttachment = [dataToDecrypt subdataWithRange:NSMakeRange(AES_CBC_IV_LENGTH, cipherTextLength)];
NSUInteger hmacOffset;
ows_sub_overflow(dataToDecrypt.length, HMAC256_OUTPUT_LENGTH, &hmacOffset);
NSData *hmac = [dataToDecrypt subdataWithRange:NSMakeRange(hmacOffset, HMAC256_OUTPUT_LENGTH)];
NSData *_Nullable paddedPlainText = [Cryptography decryptCBCMode:encryptedAttachment
key:encryptionKey
IV:iv
version:nil
HMACKey:hmacKey
HMACType:TSHMACSHA256AttachementType
matchingHMAC:hmac
digest:digest];
if (!paddedPlainText) {
OWSFailDebug(@"couldn't decrypt attachment.");
*error = SCKErrorWithCodeDescription(SCKErrorCode_FailedToDecryptMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @""));
return nil;
} else if (unpaddedSize == 0) {
// Legacy iOS clients didn't set the unpaddedSize on attachments.
// So an unpaddedSize of 0 could mean one of two things:
// [case 1] receiving a legacy attachment from before padding was introduced
// [case 2] receiving a modern attachment of length 0 that just has some null padding (e.g. an empty group sync)
__block BOOL foundNonNullByte = NO;
[paddedPlainText enumerateByteRangesUsingBlock:^(const void * _Nonnull bytes, NSRange byteRange, BOOL * _Nonnull stop) {
for (NSUInteger i = 0; i < byteRange.length; ++i) {
if (((uint8_t*)bytes)[i] != 0x00) {
foundNonNullByte = YES;
*stop = YES;
}
}
}];
if (foundNonNullByte) {
// [case 1] There was something besides 0 in our data, assume it wasn't padding.
return paddedPlainText;
} else {
// [case 2] The bytes were all 0's. We assume it was all padding and the actual
// attachment data was indeed empty. The downside here would be if a legacy client
// was intentionally sending an attachment consisting of just 0's. This seems unlikely,
// and would only affect iOS clients from before commit:
//
// 6eeb78157a044e632adc3daf6254aceacd53e335
// Author: Michael Kirk <michael.code@endoftheworl.de>
// Date: Thu Oct 26 15:08:25 2017 -0700
//
// Include size in attachment pointer
return [NSData new];
}
} else {
if (unpaddedSize > paddedPlainText.length) {
*error = SCKErrorWithCodeDescription(SCKErrorCode_FailedToDecryptMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @""));
return nil;
}
if (unpaddedSize == paddedPlainText.length) {
OWSLogInfo(@"decrypted unpadded attachment.");
return [paddedPlainText copy];
} else {
unsigned long paddingSize;
ows_sub_overflow(paddedPlainText.length, unpaddedSize, &paddingSize);
OWSLogInfo(@"decrypted padded attachment with unpaddedSize: %lu, paddingSize: %lu",
(unsigned long)unpaddedSize,
paddingSize);
return [paddedPlainText subdataWithRange:NSMakeRange(0, unpaddedSize)];
}
}
}
+ (unsigned long)paddedSize:(unsigned long)unpaddedSize
{
return MAX(541, floor( pow(1.05, ceil( log(unpaddedSize) / log(1.05)))));
}
+ (nullable NSData *)encryptAttachmentData:(NSData *)attachmentData
shouldPad:(BOOL)shouldPad
outKey:(NSData *_Nonnull *_Nullable)outKey
outDigest:(NSData *_Nonnull *_Nullable)outDigest
{
// Due to paddedSize, we need to divide by two.
if (attachmentData.length >= SIZE_MAX / 2) {
OWSLogError(@"data is too long.");
return nil;
}
NSData *iv = [Cryptography generateRandomBytes:AES_CBC_IV_LENGTH];
NSData *encryptionKey = [Cryptography generateRandomBytes:AES_KEY_SIZE];
NSData *hmacKey = [Cryptography generateRandomBytes:HMAC256_KEY_LENGTH];
// The concatenated key for storage
NSMutableData *attachmentKey = [NSMutableData data];
[attachmentKey appendData:encryptionKey];
[attachmentKey appendData:hmacKey];
*outKey = [attachmentKey copy];
// Apply any padding
unsigned long desiredSize;
if (shouldPad) {
desiredSize = [self paddedSize:attachmentData.length];
} else {
desiredSize = attachmentData.length;
}
NSMutableData *paddedAttachmentData = [attachmentData mutableCopy];
paddedAttachmentData.length = desiredSize;
// Encrypt
size_t bufferSize;
ows_add_overflow(paddedAttachmentData.length, kCCBlockSizeAES128, &bufferSize);
NSMutableData *_Nullable bufferData = [NSMutableData dataWithLength:bufferSize];
if (!bufferData) {
OWSFail(@"Failed to allocate buffer.");
}
size_t bytesEncrypted = 0;
CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt,
kCCAlgorithmAES128,
kCCOptionPKCS7Padding,
[encryptionKey bytes],
[encryptionKey length],
[iv bytes],
[paddedAttachmentData bytes],
[paddedAttachmentData length],
bufferData.mutableBytes,
bufferSize,
&bytesEncrypted);
if (cryptStatus != kCCSuccess) {
OWSLogError(@"CCCrypt failed with status: %d", (int32_t)cryptStatus);
return nil;
}
if (bufferData.length < bytesEncrypted) {
OWSFailDebug(@"bufferData has unexpected length: %lu < %lu",
(unsigned long) bufferData.length,
(unsigned long) bytesEncrypted);
return nil;
}
NSData *cipherText = [bufferData subdataWithRange:NSMakeRange(0, bytesEncrypted)];
NSMutableData *encryptedPaddedData = [NSMutableData data];
[encryptedPaddedData appendData:iv];
[encryptedPaddedData appendData:cipherText];
// compute hmac of: iv || encrypted data
NSData *_Nullable hmac =
[Cryptography truncatedSHA256HMAC:encryptedPaddedData withHMACKey:hmacKey truncation:HMAC256_OUTPUT_LENGTH];
if (!hmac) {
OWSFailDebug(@"could not compute SHA 256 HMAC.");
return nil;
}
[encryptedPaddedData appendData:hmac];
// compute digest of: iv || encrypted data || hmac
NSData *_Nullable digest = [self computeSHA256Digest:encryptedPaddedData];
if (!digest) {
OWSFailDebug(@"data is too long.");
return nil;
}
*outDigest = digest;
return [encryptedPaddedData copy];
}
#pragma mark - AES-GCM
+ (nullable AES25GCMEncryptionResult *)encryptAESGCMWithData:(NSData *)plaintext

View File

@ -1,6 +1,6 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import CommonCrypto
@ -68,19 +68,19 @@ public extension Cryptography {
guard key.count == hmacsivDataLength else { throw invalidLengthError("key") }
guard let authData = "auth".data(using: .utf8),
let Ka = computeSHA256HMAC(authData, withHMACKey: key) else {
let Ka = computeSHA256HMAC(authData, key: key) else {
throw OWSAssertionError("failed to compute Ka")
}
guard let encData = "enc".data(using: .utf8),
let Ke = computeSHA256HMAC(encData, withHMACKey: key) else {
let Ke = computeSHA256HMAC(encData, key: key) else {
throw OWSAssertionError("failed to compute Ke")
}
guard let iv = truncatedSHA256HMAC(data, withHMACKey: Ka, truncation: UInt(hmacsivIVLength)) else {
guard let iv = computeSHA256HMAC(data, key: Ka, truncatedToBytes: UInt(hmacsivIVLength)) else {
throw OWSAssertionError("failed to compute IV")
}
guard let Kx = computeSHA256HMAC(iv, withHMACKey: Ke) else {
guard let Kx = computeSHA256HMAC(iv, key: Ke) else {
throw OWSAssertionError("failed to compute Kx")
}
@ -97,21 +97,21 @@ public extension Cryptography {
guard key.count == hmacsivDataLength else { throw invalidLengthError("key") }
guard let authData = "auth".data(using: .utf8),
let Ka = computeSHA256HMAC(authData, withHMACKey: key) else {
let Ka = computeSHA256HMAC(authData, key: key) else {
throw OWSAssertionError("failed to compute Ka")
}
guard let encData = "enc".data(using: .utf8),
let Ke = computeSHA256HMAC(encData, withHMACKey: key) else {
let Ke = computeSHA256HMAC(encData, key: key) else {
throw OWSAssertionError("failed to compute Ke")
}
guard let Kx = computeSHA256HMAC(iv, withHMACKey: Ke) else {
guard let Kx = computeSHA256HMAC(iv, key: Ke) else {
throw OWSAssertionError("failed to compute Kx")
}
let decryptedData = try Kx ^ cipherText
guard let ourIV = truncatedSHA256HMAC(decryptedData, withHMACKey: Ka, truncation: UInt(hmacsivIVLength)) else {
guard let ourIV = computeSHA256HMAC(decryptedData, key: Ka, truncatedToBytes: UInt(hmacsivIVLength)) else {
throw OWSAssertionError("failed to compute IV")
}
@ -125,41 +125,48 @@ public extension Cryptography {
// SHA-256
/// Generates the SHA256 digest for a file.
class func computeSHA256DigestOfFile(at url: URL) -> Data? {
let bufferSize = 1024 * 1024
let file: FileHandle
@objc
class func computeSHA256DigestOfFile(at url: URL) throws -> Data {
let file = try FileHandle(forReadingFrom: url)
var digestContext = SHA256DigestContext()
try file.enumerateInBlocks { try digestContext.update($0) }
return try digestContext.finalize()
}
@objc
class func computeSHA256Digest(_ data: Data) -> Data? {
var digestContext = SHA256DigestContext()
do {
file = try FileHandle(forReadingFrom: url)
try digestContext.update(data)
return try digestContext.finalize()
} catch {
owsFailDebug("Cannot open file: \(error.localizedDescription)")
owsFailDebug("Failed to compute digest \(error)")
return nil
}
}
defer { file.closeFile() }
@objc
class func computeSHA256Digest(_ data: Data, truncatedToBytes: UInt) -> Data? {
guard let digest = computeSHA256Digest(data), digest.count >= truncatedToBytes else { return nil }
return digest.subdata(in: digest.startIndex..<digest.startIndex.advanced(by: Int(truncatedToBytes)))
}
var context = CC_SHA256_CTX()
CC_SHA256_Init(&context)
@objc
class func computeSHA256HMAC(_ data: Data, key: Data) -> Data? {
do {
var context = try HmacContext(key: key)
try context.update(data)
return try context.finalize()
} catch {
owsFailDebug("Failed to compute hmac \(error)")
return nil
}
}
// Read up to `bufferSize` bytes, until EOF is reached, and update SHA256 context
while autoreleasepool(invoking: {
let data = file.readData(ofLength: bufferSize)
if data.count > 0 {
data.withUnsafeBytes {
_ = CC_SHA256_Update(&context, $0.baseAddress, numericCast(data.count))
}
return true // Continue
} else {
return false // End of file
}
}) { }
// Compute the SHA256 digest
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
_ = CC_SHA256_Final(&digest, &context)
return Data(digest)
@objc
class func computeSHA256HMAC(_ data: Data, key: Data, truncatedToBytes: UInt) -> Data? {
guard let hmac = computeSHA256HMAC(data, key: key), hmac.count >= truncatedToBytes else { return nil }
return hmac.subdata(in: hmac.startIndex..<hmac.startIndex.advanced(by: Int(truncatedToBytes)))
}
}
@ -169,3 +176,488 @@ extension Data {
return Data(zip(lhs, rhs).map { $0 ^ $1 })
}
}
// MARK: - Attachments
public struct EncryptionMetadata {
public let key: Data
public let digest: Data?
public let length: Int?
public let plaintextLength: Int?
public init(key: Data, digest: Data? = nil, length: Int? = nil, plaintextLength: Int? = nil) {
self.key = key
self.digest = digest
self.length = length
self.plaintextLength = plaintextLength
}
}
public extension Cryptography {
fileprivate static let hmac256KeyLength = 32
fileprivate static let hmac256OutputLength = 32
fileprivate static let aescbcIVLength = 16
fileprivate static let aesKeySize = 32
class func paddedSize(unpaddedSize: UInt) -> UInt {
// In order to obsfucate attachment size on the wire, we round up
// attachement plaintext bytes to the nearest power of 1.05. This
// number was selected as it provides a good balance between number
// of buckets and wasted bytes on the wire.
return UInt(max(541, floor(pow(1.05, ceil(log(Double(unpaddedSize)) / log(1.05))))))
}
class func encryptAttachment(at unencryptedUrl: URL, output encryptedUrl: URL) throws -> EncryptionMetadata {
guard FileManager.default.fileExists(atPath: unencryptedUrl.path) else {
throw OWSAssertionError("Missing attachment file.")
}
let inputFile = try FileHandle(forReadingFrom: unencryptedUrl)
guard FileManager.default.createFile(
atPath: encryptedUrl.path,
contents: nil,
attributes: [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication]
) else {
throw OWSAssertionError("Cannot access output file.")
}
let outputFile = try FileHandle(forWritingTo: encryptedUrl)
let iv = generateRandomBytes(UInt(aescbcIVLength))
let encryptionKey = generateRandomBytes(UInt(aesKeySize))
let hmacKey = generateRandomBytes(UInt(hmac256KeyLength))
var hmacContext = try HmacContext(key: hmacKey)
var digestContext = SHA256DigestContext()
var cipherContext = try CipherContext(
operation: .encrypt,
algorithm: .aes,
options: .pkcs7Padding,
key: encryptionKey,
iv: iv
)
// We include our IV at the start of the file *and*
// in both the hmac and digest.
try hmacContext.update(iv)
try digestContext.update(iv)
outputFile.write(iv)
let unpaddedPlaintextLength: UInt
// Encrypt the file by enumerating blocks. We want to keep our
// memory footprint as small as possible during encryption.
do {
try inputFile.enumerateInBlocks { plaintextDataBlock in
let ciphertextBlock = try cipherContext.update(plaintextDataBlock)
try hmacContext.update(ciphertextBlock)
try digestContext.update(ciphertextBlock)
outputFile.write(ciphertextBlock)
}
// Add zero padding to the plaintext attachment data if necessary.
unpaddedPlaintextLength = UInt(inputFile.offsetInFile)
let paddedPlaintextLength = paddedSize(unpaddedSize: unpaddedPlaintextLength)
if paddedPlaintextLength > unpaddedPlaintextLength {
let ciphertextBlock = try cipherContext.update(
Data(repeating: 0, count: Int(paddedPlaintextLength - unpaddedPlaintextLength))
)
try hmacContext.update(ciphertextBlock)
try digestContext.update(ciphertextBlock)
outputFile.write(ciphertextBlock)
}
// Finalize the encryption and write out the last block.
// Every time we "update" the cipher context, it returns
// the ciphertext for the previous block so there will
// always be one block remaining when we "finalize".
let finalCiphertextBlock = try cipherContext.finalize()
try hmacContext.update(finalCiphertextBlock)
try digestContext.update(finalCiphertextBlock)
outputFile.write(finalCiphertextBlock)
}
// Calculate our HMAC. This will be used to verify the
// data after decryption.
// hmac of: iv || encrypted data
let hmac = try hmacContext.finalize()
// We write the hmac at the end of the file for the
// receiver to use for verification. We also include
// it in the digest.
try digestContext.update(hmac)
outputFile.write(hmac)
// Calculate our digest. This will be used to verify
// the data after decryption.
// digest of: iv || encrypted data || hmac
let digest = try digestContext.finalize()
return EncryptionMetadata(
key: encryptionKey + hmacKey,
digest: digest,
length: Int(outputFile.offsetInFile),
plaintextLength: Int(unpaddedPlaintextLength)
)
}
class func decryptAttachment(
at encryptedUrl: URL,
metadata: EncryptionMetadata,
output unencryptedUrl: URL
) throws {
// We require digests for all attachments.
guard let digest = metadata.digest, !digest.isEmpty else {
throw OWSAssertionError("Missing digest")
}
try decryptFile(at: encryptedUrl, metadata: metadata, output: unencryptedUrl)
}
class func decryptFile(
at encryptedUrl: URL,
metadata: EncryptionMetadata,
output unencryptedUrl: URL
) throws {
guard FileManager.default.fileExists(atPath: encryptedUrl.path) else {
throw OWSAssertionError("Missing attachment file.")
}
guard let encryptedLength = try encryptedUrl.resourceValues(forKeys: [.fileSizeKey]).fileSize,
encryptedLength >= (aescbcIVLength + hmac256OutputLength) else {
throw OWSAssertionError("Encrypted file shorter than crypto overhead")
}
let plaintextLength: UInt64?
if let length = metadata.plaintextLength, length > 0 {
plaintextLength = UInt64(length)
} else {
plaintextLength = nil
}
guard metadata.key.count == (aesKeySize + hmac256KeyLength) else {
throw OWSAssertionError("Encryption key shorter than combined key length")
}
let inputFile = try FileHandle(forReadingFrom: encryptedUrl)
guard FileManager.default.createFile(
atPath: unencryptedUrl.path,
contents: nil,
attributes: [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication]
) else {
throw OWSAssertionError("Cannot access output file.")
}
let outputFile = try FileHandle(forWritingTo: unencryptedUrl)
// In the event of any failure, we both throw *and*
// delete the partially decrypted output file.
func eraseOutputFileAndError(_ description: String) throws -> Error {
outputFile.closeFile()
try FileManager.default.removeItem(at: unencryptedUrl)
return OWSAssertionError(description)
}
// This first N bytes of the encrypted file are the IV
let iv = inputFile.readData(ofLength: Int(aescbcIVLength))
guard iv.count == aescbcIVLength else {
throw try eraseOutputFileAndError("Failed to read IV")
}
// The metadata "key" is actually a concatentation of the
// encryption key and the hmac key.
let encryptionKey = metadata.key.prefix(aesKeySize)
let hmacKey = metadata.key.suffix(hmac256KeyLength)
var hmacContext = try HmacContext(key: hmacKey)
var digestContext = metadata.digest != nil ? SHA256DigestContext() : nil
var cipherContext = try CipherContext(
operation: .decrypt,
algorithm: .aes,
options: .pkcs7Padding,
key: encryptionKey,
iv: iv
)
// Matching encryption, we must start our hmac
// and digest with the IV, since the encrypted
// file starts with the IV
try hmacContext.update(iv)
try digestContext?.update(iv)
// The last N bytes of the encrypted file is the hmac
// for the encrypted data.
let hmacOffset = UInt64(encryptedLength - hmac256OutputLength)
inputFile.seek(toFileOffset: hmacOffset)
let theirHmac = inputFile.readData(ofLength: hmac256OutputLength)
guard theirHmac.count == hmac256OutputLength else {
throw try eraseOutputFileAndError("Failed to read hmac")
}
// Move the file handle to the start of the encrypted data (after IV)
inputFile.seek(toFileOffset: UInt64(aescbcIVLength))
// Decrypt the file by enumerating blocks. We want to keep our
// memory footprint as small as possible during decryption.
do {
try inputFile.enumerateInBlocks(maxOffset: hmacOffset) { ciphertextBlock in
try hmacContext.update(ciphertextBlock)
try digestContext?.update(ciphertextBlock)
let plaintextDataBlock = try cipherContext.update(ciphertextBlock)
outputFile.write(plaintextDataBlock, truncatingAfterOffset: plaintextLength)
}
// Finalize the decryption and write out the last block.
// Every time we "update" the cipher context, it returns
// the plaintext for the previous block so there will
// always be one block remaining when we "finalize".
let plaintextDataBlock = try cipherContext.finalize()
outputFile.write(plaintextDataBlock, truncatingAfterOffset: plaintextLength)
}
// If a plaintext length was specified, validate that we actually
// received plaintext of that length. Note, some older clients do
// not tell us about the unpadded plaintext length so we cannot
// universally check this.
if let plaintextLength = plaintextLength, plaintextLength != outputFile.offsetInFile {
throw try eraseOutputFileAndError("Incorrect plaintext length.")
}
// Verify their HMAC matches our locally calculated HMAC
// hmac of: iv || encrypted data
let hmac = try hmacContext.finalize()
guard hmac.ows_constantTimeIsEqual(to: theirHmac) else {
Logger.debug("Bad hmac. Their hmac: \(theirHmac.hexadecimalString), our hmac: \(hmac.hexadecimalString)")
throw try eraseOutputFileAndError("Bad hmac")
}
// Verify their digest matches our locally calculated digest
// digest of: iv || encrypted data || hmac
if let theirDigest = metadata.digest {
guard var digestContext = digestContext else {
throw try eraseOutputFileAndError("Missing digest context")
}
try digestContext.update(hmac)
let digest = try digestContext.finalize()
guard digest.ows_constantTimeIsEqual(to: theirDigest) else {
Logger.debug("Bad digest. Their digest: \(theirDigest.hexadecimalString), our digest: \(digest.hexadecimalString)")
throw try eraseOutputFileAndError("Bad digest")
}
}
}
}
extension FileHandle {
func enumerateInBlocks(
blockSize: Int = 1024 * 1024,
maxOffset: UInt64? = nil,
block: (Data) throws -> Void
) rethrows {
// Read up to `bufferSize` bytes, until EOF is reached
while try autoreleasepool(invoking: {
var blockSize = blockSize
var hasReachedMaxOffset = false
if let maxOffset = maxOffset, (maxOffset - offsetInFile) < blockSize {
blockSize = Int(maxOffset - offsetInFile)
hasReachedMaxOffset = true
}
let data = self.readData(ofLength: blockSize)
if data.count > 0 {
try block(data)
return !hasReachedMaxOffset // Continue only if we haven't reached the max offset
} else {
return false // End of file
}
}) { }
}
func write(_ data: Data, truncatingAfterOffset maxOffset: UInt64?) {
if let maxOffset = maxOffset, (offsetInFile + UInt64(data.count)) > maxOffset {
write(data.prefix(Int(maxOffset - offsetInFile)))
} else {
write(data)
}
}
}
struct SHA256DigestContext {
private var context = CC_SHA256_CTX()
private var isFinal = false
init() {
CC_SHA256_Init(&context)
}
mutating func update(_ data: Data) throws {
try data.withUnsafeBytes { try update(bytes: $0) }
}
mutating func update(bytes: UnsafeRawBufferPointer) throws {
guard !isFinal else {
throw OWSAssertionError("Unexpectedly attempted update a finalized hmac digest")
}
CC_SHA256_Update(&context, bytes.baseAddress, numericCast(bytes.count))
}
mutating func finalize() throws -> Data {
guard !isFinal else {
throw OWSAssertionError("Unexpectedly attempted to finalize a finalized hmac digest")
}
isFinal = true
var digest = Data(repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
_ = digest.withUnsafeMutableBytes {
CC_SHA256_Final($0.baseAddress?.assumingMemoryBound(to: UInt8.self), &context)
}
return digest
}
}
struct HmacContext {
private var context = CCHmacContext()
private var isFinal = false
init(key: Data) throws {
key.withUnsafeBytes {
CCHmacInit(&context, CCHmacAlgorithm(kCCHmacAlgSHA256), $0.baseAddress, $0.count)
}
}
mutating func update(_ data: Data) throws {
try data.withUnsafeBytes { try update(bytes: $0) }
}
mutating func update(bytes: UnsafeRawBufferPointer) throws {
guard !isFinal else {
throw OWSAssertionError("Unexpectedly attempted to update a finalized hmac context")
}
CCHmacUpdate(&context, bytes.baseAddress, bytes.count)
}
mutating func finalize() throws -> Data {
guard !isFinal else {
throw OWSAssertionError("Unexpectedly to finalize a finalized hmac context")
}
isFinal = true
var mac = Data(repeating: 0, count: Cryptography.hmac256OutputLength)
mac.withUnsafeMutableBytes {
CCHmacFinal(&context, $0.baseAddress)
}
return mac
}
}
struct CipherContext {
enum Operation {
case encrypt
case decrypt
var ccValue: CCOperation {
switch self {
case .encrypt: return CCOperation(kCCEncrypt)
case .decrypt: return CCOperation(kCCDecrypt)
}
}
}
enum Algorithm {
case aes
case des
case threeDes
case cast
case rc4
case rc2
case blowfish
var ccValue: CCOperation {
switch self {
case .aes: return CCAlgorithm(kCCAlgorithmAES)
case .des: return CCAlgorithm(kCCAlgorithmDES)
case .threeDes: return CCAlgorithm(kCCAlgorithm3DES)
case .cast: return CCAlgorithm(kCCAlgorithmCAST)
case .rc4: return CCAlgorithm(kCCAlgorithmRC4)
case .rc2: return CCAlgorithm(kCCAlgorithmRC2)
case .blowfish: return CCAlgorithm(kCCAlgorithmBlowfish)
}
}
}
struct Options: OptionSet {
let rawValue: Int
static let pkcs7Padding = Options(rawValue: kCCOptionPKCS7Padding)
static let ecbMode = Options(rawValue: kCCOptionECBMode)
}
private var cryptor: CCCryptorRef?
private var isFinal = false
init(operation: Operation, algorithm: Algorithm, options: Options, key: Data, iv: Data) throws {
let result = key.withUnsafeBytes { keyBytes in
iv.withUnsafeBytes { ivBytes in
CCCryptorCreate(
operation.ccValue,
algorithm.ccValue,
CCOptions(options.rawValue),
keyBytes.baseAddress,
keyBytes.count,
ivBytes.baseAddress,
&cryptor
)
}
}
guard result == CCStatus(kCCSuccess) else {
throw OWSAssertionError("Invalid arguments provided \(result)")
}
}
mutating func update(_ data: Data) throws -> Data {
return try data.withUnsafeBytes { try update(bytes: $0) }
}
mutating func update(bytes: UnsafeRawBufferPointer) throws -> Data {
guard !isFinal else {
throw OWSAssertionError("Unexpectedly attempted to update a finalized cipher")
}
var outputLength = CCCryptorGetOutputLength(cryptor, bytes.count, true)
var outputBuffer = Data(repeating: 0, count: outputLength)
let result = outputBuffer.withUnsafeMutableBytes {
CCCryptorUpdate(cryptor, bytes.baseAddress, bytes.count, $0.baseAddress, $0.count, &outputLength)
}
guard result == CCStatus(kCCSuccess) else {
throw OWSAssertionError("Unexpected result \(result)")
}
outputBuffer.count = outputLength
return outputBuffer
}
mutating func finalize() throws -> Data {
guard !isFinal else {
throw OWSAssertionError("Unexpectedly attempted to finalize a finalized cipher")
}
isFinal = true
var outputLength = CCCryptorGetOutputLength(cryptor, 0, true)
var outputBuffer = Data(repeating: 0, count: outputLength)
let result = outputBuffer.withUnsafeMutableBytes {
CCCryptorFinal(cryptor, $0.baseAddress, $0.count, &outputLength)
}
guard result == CCStatus(kCCSuccess) else {
throw OWSAssertionError("Unexpected result \(result)")
}
outputBuffer.count = outputLength
return outputBuffer
}
}

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import <UIKit/UIKit.h>
@ -11,5 +11,5 @@ FOUNDATION_EXPORT double SignalCoreKitVersionNumber;
FOUNDATION_EXPORT const unsigned char SignalCoreKitVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <SignalCoreKit/PublicHeader.h>
#import <SignalCoreKit/Cryptography.h>
#import <SignalCoreKit/SCKError.h>

View File

@ -1,10 +1,11 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import "Cryptography.h"
#import "Randomness.h"
#import <SignalCoreKit/NSData+OWS.h>
#import <SignalCoreKit/SignalCoreKit-Swift.h>
#import <XCTest/XCTest.h>
NS_ASSUME_NONNULL_BEGIN
@ -15,130 +16,8 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark -
@interface Cryptography (Test)
+ (NSData *)truncatedSHA256HMAC:(NSData *)dataToHMAC withHMACKey:(NSData *)HMACKey truncation:(int)bytes;
+ (NSData *)encryptCBCMode:(NSData *)dataToEncrypt
withKey:(NSData *)key
withIV:(NSData *)iv
withVersion:(NSData *)version
withHMACKey:(NSData *)hmacKey
withHMACType:(TSMACType)hmacType
computedHMAC:(NSData **)hmac;
+ (NSData *)decryptCBCMode:(NSData *)dataToDecrypt
key:(NSData *)key
IV:(NSData *)iv
version:(NSData *)version
HMACKey:(NSData *)hmacKey
HMACType:(TSMACType)hmacType
matchingHMAC:(NSData *)hmac;
@end
#pragma mark -
@implementation CryptographyTests
- (void)testEncryptAttachmentData
{
NSString *plainText = @"SGF3YWlpIGlzIEF3ZXNvbWUh";
NSData *plainTextData = [NSData dataFromBase64String:plainText];
// Sanity
XCTAssertNotNil(plainTextData);
NSData *generatedKey;
NSData *generatedDigest;
NSData *cipherText =
[Cryptography encryptAttachmentData:plainTextData shouldPad:YES outKey:&generatedKey outDigest:&generatedDigest];
NSError *error;
NSData *_Nullable decryptedData = [Cryptography decryptAttachment:cipherText
withKey:generatedKey
digest:generatedDigest
unpaddedSize:(UInt32)plainTextData.length
error:&error];
XCTAssertNil(error);
XCTAssertEqualObjects(plainTextData, decryptedData);
}
- (void)testEncryptAttachmentDataWithBadUnpaddedSize
{
NSString *plainText = @"SGF3YWlpIGlzIEF3ZXNvbWUh";
NSData *plainTextData = [NSData dataFromBase64String:plainText];
// Sanity
XCTAssertNotNil(plainTextData);
NSData *generatedKey;
NSData *generatedDigest;
NSData *cipherText =
[Cryptography encryptAttachmentData:plainTextData shouldPad:YES outKey:&generatedKey outDigest:&generatedDigest];
NSError *error;
NSData *_Nullable decryptedData = [Cryptography decryptAttachment:cipherText
withKey:generatedKey
digest:generatedDigest
unpaddedSize:(UInt32)cipherText.length + 1
error:&error];
XCTAssertNotNil(error);
XCTAssertNil(decryptedData);
}
- (void)testDecryptAttachmentWithBadKey
{
NSString *plainText = @"SGF3YWlpIGlzIEF3ZXNvbWUh";
NSData *plainTextData = [NSData dataFromBase64String:plainText];
// Sanity
XCTAssertNotNil(plainTextData);
NSData *generatedKey;
NSData *generatedDigest;
NSData *cipherText =
[Cryptography encryptAttachmentData:plainTextData shouldPad:YES outKey:&generatedKey outDigest:&generatedDigest];
NSData *badKey = [Cryptography generateRandomBytes:64];
NSError *error;
XCTAssertThrows([Cryptography decryptAttachment:cipherText
withKey:badKey
digest:generatedDigest
unpaddedSize:(UInt32)plainTextData.length
error:&error]);
}
- (void)testDecryptAttachmentWithBadDigest
{
NSString *plainText = @"SGF3YWlpIGlzIEF3ZXNvbWUh";
NSData *plainTextData = [NSData dataFromBase64String:plainText];
// Sanity
XCTAssertNotNil(plainTextData);
NSData *generatedKey;
NSData *generatedDigest;
NSData *_Nullable cipherText =
[Cryptography encryptAttachmentData:plainTextData shouldPad:YES outKey:&generatedKey outDigest:&generatedDigest];
XCTAssertNotNil(cipherText);
NSData *badDigest = [Cryptography generateRandomBytes:32];
NSError *error;
XCTAssertThrows([Cryptography decryptAttachment:cipherText
withKey:generatedKey
digest:badDigest
unpaddedSize:(UInt32)plainTextData.length
error:&error]);
}
- (void)testComputeSHA256Digest
{
NSString *plainText = @"SGF3YWlpIGlzIEF3ZXNvbWUh";

View File

@ -1,6 +1,6 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import XCTest
@ -86,4 +86,150 @@ class CryptographyTestsSwift: XCTestCase {
XCTAssertEqual(iv, Data.data(fromHex: "f27036915a60d704b04d452ef0d55a5d")!)
XCTAssertEqual(cipherText, Data.data(fromHex: "1668e7d91339daba9c950d985b7556471d13cc609e59eec62fb1ce27f5c5a342")!)
}
func test_attachmentEncryptionAndDecryption() throws {
let temporaryDirectory = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let plaintextFile = temporaryDirectory.appendingPathComponent(UUID().uuidString)
let encryptedFile = temporaryDirectory.appendingPathComponent(UUID().uuidString)
let plaintextData = Data.data(fromHex: "6E6F7261207761732068657265")!
try plaintextData.write(to: plaintextFile)
let metadata = try Cryptography.encryptAttachment(at: plaintextFile, output: encryptedFile)
try FileManager.default.removeItem(at: plaintextFile)
try Cryptography.decryptAttachment(
at: encryptedFile,
metadata: metadata,
output: plaintextFile
)
let decryptedData = try Data(contentsOf: plaintextFile)
XCTAssertEqual(plaintextData, decryptedData)
}
func test_attachmentEncryptionAndDecryptionWithGarbageInFile() throws {
let temporaryDirectory = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let plaintextFile = temporaryDirectory.appendingPathComponent(UUID().uuidString)
let encryptedFile = temporaryDirectory.appendingPathComponent(UUID().uuidString)
let plaintextData = Data.data(fromHex: "6E6F7261207761732068657265")!
try plaintextData.write(to: plaintextFile)
let metadata = try Cryptography.encryptAttachment(at: plaintextFile, output: encryptedFile)
try FileManager.default.removeItem(at: plaintextFile)
try Cryptography.generateRandomBytes(1024).write(to: plaintextFile)
try Cryptography.decryptAttachment(
at: encryptedFile,
metadata: metadata,
output: plaintextFile
)
let decryptedData = try Data(contentsOf: plaintextFile)
XCTAssertEqual(plaintextData, decryptedData)
}
func test_attachmentDecryptionWithBadUnpaddedSize() throws {
let temporaryDirectory = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let plaintextFile = temporaryDirectory.appendingPathComponent(UUID().uuidString)
let encryptedFile = temporaryDirectory.appendingPathComponent(UUID().uuidString)
let plaintextData = Data.data(fromHex: "6E6F7261207761732068657265")!
try plaintextData.write(to: plaintextFile)
let metadata = try Cryptography.encryptAttachment(at: plaintextFile, output: encryptedFile)
let invalidMetadata = EncryptionMetadata(
key: metadata.key,
digest: metadata.digest,
length: metadata.length,
plaintextLength: metadata.length! + 1
)
try FileManager.default.removeItem(at: plaintextFile)
XCTAssertThrowsError(try Cryptography.decryptAttachment(
at: encryptedFile,
metadata: invalidMetadata,
output: plaintextFile
))
XCTAssertFalse(FileManager.default.fileExists(atPath: plaintextFile.path))
}
func test_attachmentDecryptionWithBadKey() throws {
let temporaryDirectory = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let plaintextFile = temporaryDirectory.appendingPathComponent(UUID().uuidString)
let encryptedFile = temporaryDirectory.appendingPathComponent(UUID().uuidString)
let plaintextData = Data.data(fromHex: "6E6F7261207761732068657265")!
try plaintextData.write(to: plaintextFile)
let metadata = try Cryptography.encryptAttachment(at: plaintextFile, output: encryptedFile)
let invalidMetadata = EncryptionMetadata(
key: Cryptography.generateRandomBytes(64),
digest: metadata.digest,
length: metadata.length,
plaintextLength: metadata.plaintextLength
)
try FileManager.default.removeItem(at: plaintextFile)
XCTAssertThrowsError(try Cryptography.decryptAttachment(
at: encryptedFile,
metadata: invalidMetadata,
output: plaintextFile
))
XCTAssertFalse(FileManager.default.fileExists(atPath: plaintextFile.path))
}
func test_attachmentDecryptionWithMissingDigest() throws {
let temporaryDirectory = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let plaintextFile = temporaryDirectory.appendingPathComponent(UUID().uuidString)
let encryptedFile = temporaryDirectory.appendingPathComponent(UUID().uuidString)
let plaintextData = Data.data(fromHex: "6E6F7261207761732068657265")!
try plaintextData.write(to: plaintextFile)
let metadata = try Cryptography.encryptAttachment(at: plaintextFile, output: encryptedFile)
let invalidMetadata = EncryptionMetadata(
key: metadata.key,
digest: nil,
length: metadata.length,
plaintextLength: metadata.plaintextLength
)
try FileManager.default.removeItem(at: plaintextFile)
XCTAssertThrowsError(try Cryptography.decryptAttachment(
at: encryptedFile,
metadata: invalidMetadata,
output: plaintextFile
))
XCTAssertFalse(FileManager.default.fileExists(atPath: plaintextFile.path))
}
func test_fileEncryptionAndDecryptionMissingDigest() throws {
let temporaryDirectory = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let plaintextFile = temporaryDirectory.appendingPathComponent(UUID().uuidString)
let encryptedFile = temporaryDirectory.appendingPathComponent(UUID().uuidString)
let plaintextData = Data.data(fromHex: "6E6F7261207761732068657265")!
try plaintextData.write(to: plaintextFile)
let metadata = try Cryptography.encryptAttachment(at: plaintextFile, output: encryptedFile)
let metadataWithoutDigest = EncryptionMetadata(
key: metadata.key,
digest: nil,
length: metadata.length,
plaintextLength: metadata.plaintextLength
)
try FileManager.default.removeItem(at: plaintextFile)
try Cryptography.decryptFile(
at: encryptedFile,
metadata: metadataWithoutDigest,
output: plaintextFile
)
let decryptedData = try Data(contentsOf: plaintextFile)
XCTAssertEqual(plaintextData, decryptedData)
}
}