Add streaming encryption and decryption APIs
This commit is contained in:
parent
442e2d544a
commit
64410ab6b3
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user