-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CryptorModule #144
CryptorModule #144
Conversation
* Reusing existing CryptoStream + CryptoInputStream by removing Crypto dependency from them * Moved old encryption/decryption to LegacyCryptor * Introduced new AESCBCCryptor * Introduced new CryptorModule instead of Crypto * Fixing tests
…readable error descriptions
* Prevent from decoding & encoding empty Data & Files * Added inline documentation for public items
case encryptionError | ||
case decryptionError | ||
case unknownCryptorError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jguz-pubnub Should we try to be consistent with other cases in class? The class itself already specifies that it will be errors, and the *Error
suffix won't have much meaning. Other cases which report any troubles have *Failure
suffix.
public override func setup() { | ||
startCucumberHookEventsListening() | ||
|
||
var cryptorKind: String = "" | ||
var cipherKey: String = "" | ||
var randomIV: Bool = true | ||
var otherCryptors: [String] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setup
called only once, and those variables won't be reset for each scenario. I suggest moving them into handleBeforeHook
:
public override func setup() { | |
startCucumberHookEventsListening() | |
var cryptorKind: String = "" | |
var cipherKey: String = "" | |
var randomIV: Bool = true | |
var otherCryptors: [String] = [] | |
var cryptorKind: String! | |
var cipherKey: String! | |
var randomIV: Bool = true | |
var otherCryptors: [String] = [] | |
override public func handleBeforeHook() { | |
cryptorKind = "" | |
cipherKey = "" | |
randomIV = true | |
otherCryptors = [] | |
super.handleBeforeHook() | |
} | |
public override func setup() { | |
startCucumberHookEventsListening() |
When("I decrypt '(.*)' file") { args, userInfo in | ||
let fileName = args?.first ?? "" | ||
let localUrl = self.localUrl(for: fileName) | ||
let inputStream = InputStream(url: localUrl)! | ||
let outputUrl = self.generateTestOutputUrl() | ||
|
||
let cryptorModule = self.createCryptorModule(cryptorKind, key: cipherKey, withRandomIV: randomIV) | ||
let encryptedStreamData = EncryptedStreamResult(stream: inputStream, contentLength: localUrl.sizeOf) | ||
let decryptingRes = cryptorModule.decrypt(stream: encryptedStreamData, to: outputUrl) | ||
|
||
Then("I receive '(.*)'") { thenArgs, _ in | ||
switch thenArgs?.first ?? "" { | ||
case "unknown cryptor error": | ||
XCTAssertTrue(self.failureIfAny(from: decryptingRes)?.reason == .unknownCryptorError) | ||
case "decryption error": | ||
XCTAssertTrue(self.failureIfAny(from: decryptingRes)?.reason == .decryptionError) | ||
case "success": | ||
XCTAssertNotNil(try? decryptingRes.get()) | ||
default: | ||
XCTFail("Unsupported outcome") | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't it be like follows and decryptingRes
be declared as instance variable (as above):
When("I decrypt '(.*)' file") { args, userInfo in | |
let fileName = args?.first ?? "" | |
let localUrl = self.localUrl(for: fileName) | |
let inputStream = InputStream(url: localUrl)! | |
let outputUrl = self.generateTestOutputUrl() | |
let cryptorModule = self.createCryptorModule(cryptorKind, key: cipherKey, withRandomIV: randomIV) | |
let encryptedStreamData = EncryptedStreamResult(stream: inputStream, contentLength: localUrl.sizeOf) | |
let decryptingRes = cryptorModule.decrypt(stream: encryptedStreamData, to: outputUrl) | |
Then("I receive '(.*)'") { thenArgs, _ in | |
switch thenArgs?.first ?? "" { | |
case "unknown cryptor error": | |
XCTAssertTrue(self.failureIfAny(from: decryptingRes)?.reason == .unknownCryptorError) | |
case "decryption error": | |
XCTAssertTrue(self.failureIfAny(from: decryptingRes)?.reason == .decryptionError) | |
case "success": | |
XCTAssertNotNil(try? decryptingRes.get()) | |
default: | |
XCTFail("Unsupported outcome") | |
} | |
} | |
} | |
When("I decrypt '(.*)' file") { args, userInfo in | |
let fileName = args?.first ?? "" | |
let localUrl = self.localUrl(for: fileName) | |
let inputStream = InputStream(url: localUrl)! | |
let outputUrl = self.generateTestOutputUrl() | |
let cryptorModule = self.createCryptorModule(cryptorKind, key: cipherKey, withRandomIV: randomIV) | |
let encryptedStreamData = EncryptedStreamResult(stream: inputStream, contentLength: localUrl.sizeOf) | |
let decryptingRes = cryptorModule.decrypt(stream: encryptedStreamData, to: outputUrl) | |
} | |
Then("I receive '(.*)'") { thenArgs, _ in | |
switch thenArgs?.first ?? "" { | |
case "unknown cryptor error": | |
XCTAssertTrue(self.failureIfAny(from: decryptingRes)?.reason == .unknownCryptorError) | |
case "decryption error": | |
XCTAssertTrue(self.failureIfAny(from: decryptingRes)?.reason == .decryptionError) | |
case "success": | |
XCTAssertNotNil(try? decryptingRes.get()) | |
default: | |
XCTFail("Unsupported outcome") | |
} | |
} |
When("I encrypt '(.*)' file as 'binary'") { args, userInfo in | ||
let fileName = args?.first ?? "" | ||
let cryptorModule = self.createCryptorModule(cryptorKind, key: cipherKey, withRandomIV: randomIV) | ||
let localFileUrl = self.localUrl(for: fileName) | ||
let inputData = try! Data(contentsOf: localFileUrl) | ||
let encryptedDataRes = cryptorModule.encrypt(data: inputData) | ||
|
||
Then("Successfully decrypt an encrypted file with legacy code") { _, _ in | ||
let decryptedData = try! cryptorModule.decrypt(data: try! encryptedDataRes.get()).get() | ||
XCTAssertEqual(inputData, decryptedData) | ||
} | ||
Then("I receive 'encryption error'") { _, _ in | ||
guard case .failure(let failure) = encryptedDataRes else { | ||
XCTFail("Encryption error is expected"); return; | ||
} | ||
XCTAssertTrue(failure.reason == .encryptionError) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know that it is possible to declare it this way, but it makes create false impression that nested steps definitions will be related only to this When
:
When("I encrypt '(.*)' file as 'binary'") { args, userInfo in | |
let fileName = args?.first ?? "" | |
let cryptorModule = self.createCryptorModule(cryptorKind, key: cipherKey, withRandomIV: randomIV) | |
let localFileUrl = self.localUrl(for: fileName) | |
let inputData = try! Data(contentsOf: localFileUrl) | |
let encryptedDataRes = cryptorModule.encrypt(data: inputData) | |
Then("Successfully decrypt an encrypted file with legacy code") { _, _ in | |
let decryptedData = try! cryptorModule.decrypt(data: try! encryptedDataRes.get()).get() | |
XCTAssertEqual(inputData, decryptedData) | |
} | |
Then("I receive 'encryption error'") { _, _ in | |
guard case .failure(let failure) = encryptedDataRes else { | |
XCTFail("Encryption error is expected"); return; | |
} | |
XCTAssertTrue(failure.reason == .encryptionError) | |
} | |
} | |
When("I encrypt '(.*)' file as 'binary'") { args, userInfo in | |
let fileName = args?.first ?? "" | |
let cryptorModule = self.createCryptorModule(cryptorKind, key: cipherKey, withRandomIV: randomIV) | |
let localFileUrl = self.localUrl(for: fileName) | |
let inputData = try! Data(contentsOf: localFileUrl) | |
let encryptedDataRes = cryptorModule.encrypt(data: inputData) | |
} | |
Then("Successfully decrypt an encrypted file with legacy code") { _, _ in | |
let decryptedData = try! cryptorModule.decrypt(data: try! encryptedDataRes.get()).get() | |
XCTAssertEqual(inputData, decryptedData) | |
} | |
Then("I receive 'encryption error'") { _, _ in | |
guard case .failure(let failure) = encryptedDataRes else { | |
XCTFail("Encryption error is expected"); return; | |
} | |
XCTAssertTrue(failure.reason == .encryptionError) | |
} |
if a suggestion will be included, it will require that encryptedDataRes
to be declared as instance variable.
public var cipherKey: Crypto? | ||
public var cryptorModule: CryptorModule? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this change be a breaking change?
Can we mark it as deprecated and, if users provide it, use Crypto
field values to construct CryptoModule
under the hood and use it further with endpoints?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be possible I think, will restore Crypto
and its fields
iv = try CryptorVector.fixed.data() | ||
cipherText = data.data | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we have some more check here for case, when cipherText
is empty after we extracted IV from it?
default: | ||
continuationStream = MultipartInputStream( | ||
inputStreams: [ | ||
InputStream(data: possibleHeaderBytes.suffix(from: noOfBytesProcessedByParser)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just use noOfBytesProcessedByParser
here instead of suffix
offset computation? Shouldn't this variable store data which hasn't been processed as header?
|
||
/// A stream that provides read-only stream functionality while performing crypto operations | ||
public class CryptoInputStream: InputStream { | ||
// swiftlint:disable:previous type_body_length | ||
|
||
public struct DataSource { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, right now CrytoInputStream will work only for our cryptors? In future, users also would like to have some help from use in handling stream chunks reading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still preserved this class as public
so customers can use it. What's changed is the input that this class takes. I decoupled "old" Crypto
from this class so now customers may create its instances without any dependencies to struct/classes we're going to deprecate.
let blockSize: Int | ||
|
||
func outputSize(from inputBytes: Int) -> Int { | ||
inputBytes + (blockSize - (inputBytes % blockSize)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CCCrypto actually can tell you what resulting size you will get (depending on from operation type and source data size).
if includeInitializationVectorInContent { | ||
rawDataLength = contentLength - (operation == .decrypt ? crypto.cipher.blockSize : 0) | ||
// The estimated content length is the IV length plus the crypted length | ||
estimatedCryptoCount = crypto.cipher.blockSize + crypto.cipher.outputSize(from: rawDataLength) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned above, CCCrypto
can help with estimated output size computation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved it from the current master implementation that's available here. The reason I moved this code was not to alter/modify CryptoStream
or CryptoInputStream
already present before the more it's necessary.
CryptoStream looks like a wrapper for Swift's CommonCrypto operations. In general, I agree with you it's fine to use CommonCrypto's operations directly in the CryptoInputStream
implementation - since CryptoInputStream
is already tightly coupled with Swift's CommonCrypto, so IMHO there's nothing that CryptoStream abstracts away.
I can try to use CCCOperations directly in CryptoInputStream
, but I'm not sure yet what would be the scope of changes after introducing this idea.
* Fixing readability of contract tests * Fixing reasons (PubNubError) for Crypto errors * Fixed representation for the CryptorHeader v1 * Restoring PubNub's extension for encryption/decryption * Restoring original Crypto
* Returning MultipartInputStream as the result of stream encryption * Removing EncryptedStreamDataResult
/// - Returns: A success, storing an ``EncryptedStreamResult`` value if operation succeeds. Otherwise, a failure storing `PubNubError` is returned | ||
public func encrypt(stream: InputStream, contentLength: Int) -> Result<EncryptedStreamResult, PubNubError> { | ||
/// - Returns: A success, storing a `MultipartInputStream` value if operation succeeds. Otherwise, a failure storing `PubNubError` is returned | ||
public func encrypt(stream: InputStream, contentLength: Int) -> Result<MultipartInputStream, PubNubError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible in case of success to return InputStream
(caller shouldn't be aware about actual implementation)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is also used for File uploading, so we also need to have encrypted stream length to set proper value for HTTP Content-Header
field. Where do you store such length?
@@ -28,16 +28,18 @@ | |||
import Foundation | |||
|
|||
/// An `InputStream` that can combine multiple streams into a single stream | |||
class MultipartInputStream: InputStream { | |||
public class MultipartInputStream: InputStream { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to make it public? Will be there any benefits for users to using it as MultipartInputStream
?
Earlier, this class was created to provide support of multipart-form body, which may consist of sections represented by separate streams with data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it was your idea during the last review 🙂 The whole issue is about setting Content-Length
when uploading a File, so CryptorModule
method responsible for stream encryption has to return something more than InputStream
only. Where do you prefer having encrypted stream length stored?
guard headerLength + dataLength < buffer.count else { | ||
throw PubNubError(.decryptionFailure, additional: ["Cannot extract metadata for CryptorHeader v1"]) | ||
} | ||
// Extracts Cryptor-defined data from the supplied buffer | ||
cryptorDefinedData = buffer.subdata(in: headerLength..<headerLength + dataLength) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a note, that there can be no cryptor-defined data at all or custom implementation will handle it in a different way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I will rework that when implementing another Cryptor. Fortunately, that's the internal implementation so it won't break customers in terms of any API. The question is where do you extract metadata
for V1
header? Is it inside CryptorModule
or somewhere else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since CryptoModule
after data pre-processing (header extraction) calls to actual cryptor implementations which expect specific types (encrypt expects Data
and decrypt expects EncrpyptedData
which contains cryptor-defined portion of data and encrypted data). It is module who extracts metadata
and puts it into EncrpyptedData
.
Co-authored-by: Serhii Mamontov <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
0b4787e
to
5d2c954
Compare
@pubnub-release-bot release as v6.2.0 |
🛑 Build failed. Please check release build for details 🛑 |
fix(crypto): Improve security of crypto implementation
Improved security of crypto implementation by adding AES-CBC cryptor
feat(crypto): Add CryptorModule
Add CryptorModule that allows configuring SDK to encrypt and decrypt messages