Signal-iOS/SignalServiceKit/Util/StreamTransform/Output/EncryptingStreamTransform.swift

88 lines
3.0 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalCoreKit
public class EncryptingStreamTransform: StreamTransform, FinalizableStreamTransform {
private var cipherContext: CipherContext
private var hmacContext: HmacContext
private let iv: Data
private let encryptionKey: Data
private let hmacKey: Data
public var hasPendingBytes: Bool { return false }
private var finalized = false
public var hasFinalized: Bool { finalized }
init(iv: Data, encryptionKey: Data, hmacKey: Data) throws {
self.iv = iv
self.encryptionKey = encryptionKey
self.hmacKey = hmacKey
self.hmacContext = try HmacContext(key: hmacKey)
self.cipherContext = try CipherContext(
operation: .encrypt,
algorithm: .aes,
options: .pkcs7Padding,
key: encryptionKey,
iv: iv
)
}
public func transform(data: Data) throws -> Data {
// Current message backup format doesn't output the iv since
// it's derived from the encryption material. If this is used
// for attachments, the option to include the iv in the header
// will need to be passed in as part of the initialization.
// Get the next block of ciphertext
let ciphertextBlock = try cipherContext.update(data)
// If a small amount of data is encrypted, it may not be enought
// to complete a full ciphertext block. If that's the case,
// there's a risk of writing an empty block to the output stream.
// Writing empty/nil data to an output stream will be interpreted
// as closing the stream, so skip the write if the block is empty.
// See: Compression.OutputFilter.write() for a note on this behavior.
if ciphertextBlock.count > 0 {
// Update the hmac with the new block
try hmacContext.update(ciphertextBlock)
}
return ciphertextBlock
}
public func finalize() throws -> Data {
guard !finalized else { return Data() }
finalized = true
// 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()
// Calculate our HMAC. This will be used to verify the
// data after decryption.
// hmac of: iv || encrypted data
try hmacContext.update(finalCiphertextBlock)
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.
var footer = Data()
footer.append(finalCiphertextBlock)
footer.append(hmac)
return footer
}
public func readBufferedData() throws -> Data { Data() }
}