-
-
Notifications
You must be signed in to change notification settings - Fork 539
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
Encrypt/decrypt command #1127
Encrypt/decrypt command #1127
Conversation
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.
When running locally I was not sure if it worked, I did not get any feedback. Can you add some logging?
I agree with your implementation of CryptoSwift
however OpenSSL would have not created overhead, we would likely either rely on the c-library or to shell out to the command line both of which are fairly trivial for what we need to do.
It's really nice to see where Tuist is going with these capabilities, it's removing some of the complexity we have to deal with. At the moment we use fastlane
and match
to manage the certificates, we can take some inspiration from those tools about furthering the capabilities of this.
Great work @fortmarek
let arguments = Array(processArguments().dropFirst()) | ||
guard let argumentName = arguments.first else { return nil } | ||
let subparser = try parser.parse([argumentName]).subparser(parser) | ||
if let command = commands.first(where: { type(of: $0).command == subparser }) { | ||
return try command.parse(with: parser, arguments: arguments) | ||
} | ||
return try parser.parse(arguments) | ||
return (try parser.parse(arguments), parser) |
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.
parser is an instance on CommandRegistry
, I'm not sure what the benefit of returning it here is?
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.
SigningCommand
returns a custom parser to make it easier to parse subcommands, I'll try to revisit it if we can make it work without creating a new one
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.
Unfortunately, I could not do it -> the reason is because here I need to trigger a command which is found by subparser
. subparser
can be only one word, thus I need a custom parser that is tuist signing
and then I can successfully find the subcommand.
I am not happy with this extra logic, but we should start using the new Swift argument parser sooner than later which supports subcommands out of the box, so maybe perfecting the mechanism might be lost work, so I'd prefer keeping it as it is.
let allCommands = commands + commands.flatMap { $0.subcommands } | ||
if let command = allCommands.first(where: { type(of: $0).command == subparser }) { |
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.
Why do we need to ask all commands for their subcommand?
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 did not want to check here just for SigningCommand
, though it would be possible
pathArgument = subParser.add(option: "--path", | ||
shortName: "-p", | ||
kind: String.self, | ||
usage: "The path to the folder where the template will be generated (Default: Current directory).", |
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 usage text is incorrect, how about:
The path to the folder containing the encrypted certificates
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.
Copy & pasting programming
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.
Busted 🚓 👮
var description: String { | ||
switch self { | ||
case .failedToEncrypt: | ||
return "Encryption failed" |
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 provide more information to the end-user about why the encryption failed? This might help when debugging issues related to signing.
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.
Agree. We should make it easier for users to understand what's happening and for us to help them debug the issues.
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 have tried my best to describe what might have gone wrong
let blockSize = 16 | ||
var iv = Data(repeating: 0, count: blockSize) | ||
let result = iv.withUnsafeMutableBytes { bytes -> Int32 in | ||
guard let baseAddress = bytes.baseAddress else { return 0 } |
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.
It would be better to crash/throw here then to generate a zero iv
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.
Agreed 👍
guard let baseAddress = bytes.baseAddress else { return 0 } | ||
return SecRandomCopyBytes(kSecRandomDefault, blockSize, baseAddress) | ||
} | ||
guard result == errSecSuccess else { throw SigningCipherError.failedToEncrypt } |
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 is where you could pass the result, so we can print it out to the end-user. The error code might be good enough, but KeychainAccess went one step further and provided a description for each message - I will leave it up to you to consider.
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 am trying to parse the error and if that fails, I print out the error code
} | ||
|
||
try zip(decipheredKeys, signingKeyFiles).forEach { | ||
try $0.write(to: $1.url) |
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.
It might be a good idea to introduce some logging so that the user gets some feedback as the work for encrypt/decrypt is going on. You can introduce debug
messages for more detailed logs which only get printed when --verbose
is passed to any of the commands - this will help users of Tuist to debug what is wrong.
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.
You're definitely right, I have not got used to using it just yet, but I definitely should since it might be crucial for us and for the end user!
|
||
/// - Returns: Master key data | ||
private func masterKey(from signingDirectory: AbsolutePath) throws -> Data { | ||
let masterKeyFile = signingDirectory.appending(component: "master.key") |
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 worth creating a constant for this, since it's used in two places and is critical to the algorithm
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.
Done 👍
bundleId: "io.tuist.SignApp", | ||
infoPlist: "Info.plist", | ||
sources: "App/**", | ||
dependencies: [])) |
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 project does not generate by default, there's an extra )
$ /usr/bin/xcrun swiftc --driver-mode=swift -suppress-warnings -I /Users/oliver/Library/Developer/Xcode/DerivedData/tuist-edomxougefoacwakknnztphpgmtm/Build/Products/Debug -L /Users/oliver/Library/Developer/Xcode/DerivedData/tuist-edomxougefoacwakknnztphpgmtm/Build/Products/Debug -F /Users/oliver/Library/Developer/Xcode/DerivedData/tuist-edomxougefoacwakknnztphpgmtm/Build/Products/Debug -lProjectDescription /Users/oliver/Development/Tuist/fixtures/ios_app_with_signing/Project.swift --tuist-dump
/Users/oliver/Development/Tuist/fixtures/ios_app_with_signing/Project.swift:11:51: error: expected ',' separator
dependencies: []))
^
,
/Users/oliver/Development/Tuist/fixtures/ios_app_with_signing/Project.swift:11:51: error: expected expression in container literal
dependencies: []))
^
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.
Oops, forgot to check, good catch 👍 This should have a fixture test sooner than later to catch these as soon as possible
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.
What are you eating lately @fortmarek 😜, you are full-speed shipping lots of great features nicely implemented. One scenario that popped in my mind and that it'd be great to handle is the following:
As a user, I generate and place the certificates and profiles under Tuist/Signing
, and when I run encrypt, there's no master.key
. I think in that case we should generate a random one and use that to encrypt the files.
Also, the other scenario that I'm thinking about is. What if I want to "vendor" some of the profiles and certificates that are already in my system. Where are the profiles? How do I take the certificate from my keychain? We could provide the following command:
tuist signing vendor
The command would run only if the user is running Tuist in a interactive shell and would do the following:
- Get the list of signing certificates and ask the user for which ones they'd like to vendor.
- Get the list of profiles and ask the user for which ones they'd like to vendor.
- Place them under
Tuist/Signing
and encrypt them. - 🤯
pathArgument = subParser.add(option: "--path", | ||
shortName: "-p", | ||
kind: String.self, | ||
usage: "The path to the folder where the template will be generated (Default: Current directory).", |
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.
Copy & pasting programming
@@ -15,7 +15,7 @@ public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating { | |||
private let rootDirectoryLocator: RootDirectoryLocating | |||
|
|||
/// Default constructor. | |||
public init(rootDirectoryLocator: RootDirectoryLocating = RootDirectoryLocator.shared) { |
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.
What about removing .shared
from RootDirectoryLocator
. In hindsight, I think it was a good idea introducing that there because that class holds state, and that might introduce side effects to both the code and the tests.
var description: String { | ||
switch self { | ||
case .failedToEncrypt: | ||
return "Encryption failed" |
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.
Agree. We should make it easier for users to understand what's happening and for us to help them debug the issues.
case let .masterKeyNotFound(masterKeyPath): | ||
return "Could not find master.key at \(masterKeyPath.pathString)" | ||
case let .rootDirectoryNotFound(fromPath): | ||
return "Could not find root directory from \(fromPath.pathString)" |
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.
What is root
directory?
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.
Changed to signingDirectory
as that makes more sense in this context
let signingDirectory = rootDirectory.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) | ||
let masterKey = try self.masterKey(from: signingDirectory) | ||
// Find all files in `signingDirectory` with the exception of "master.key" | ||
let signingKeyFiles = FileHandler.shared.glob(signingDirectory, glob: "**/*") |
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'd start by only encrypting/decrypting files under Signing
. We want to be conventional about the structure of the directory and how the certificates and profiles are named and structure. If we follow the following convention there's no need to support subdirectories recursively:
Signing/
Debug.cert.enc
Release.cert.enc
MyApp.Debug.mobileprovision.enc
MyWatchApp.Debug.mobileprovision.enc
master.key
Actually... after giving it another thought, I think we master.key
can be under Tuist. That way we can use the key in the future to encrypt/decrypt other things. Something along the lines of:
master.key
Signing/
Debug.cert.enc
Release.cert.enc
MyApp.Debug.mobileprovision.enc
MyWatchApp.Debug.mobileprovision.enc
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.
Extract master.key
and stopped parsing the subdirectories, your points make sense to me 👍
Thanks @ollie and @pepibumur for the comprehensive reviews! I hope I have resolved most of the points you have raised. As for the feature suggestions of autogeneration of master key and I am stoked about what's next, the next PR for signing should be the most interesting one since it's where the logic of implementing signing comes in 👀 |
Codecov Report
@@ Coverage Diff @@
## master #1127 +/- ##
=======================================
Coverage 76.88% 76.88%
=======================================
Files 273 273
Lines 9620 9620
=======================================
Hits 7396 7396
Misses 2224 2224 Continue to review full report at Codecov.
|
// MARK: - Attributes | ||
|
||
static let command = "signing" | ||
static let overview = "Signing command" |
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 expand this a bit?:
A set of commands for signing-related operations.
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.
Good thinking, will do 👍
case masterKeyNotFound(AbsolutePath) | ||
case signingDirectoryNotFound(AbsolutePath) | ||
|
||
var type: ErrorType { .abort } |
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'd implement all the cases. That way, if someone adds a new case in the future, they'll forced to think about the type.
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 just declares what the ErrorType
of SigningCipher
is, I suppose a switch does not make sense in this case.
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.
Right, but as a developers we need to think about what type of error we are throwing. If we don't use a switch clause and rather always return the same value, when I add a new value I won't be prompted to think about the type of error of the new case.
var type: ErrorType {
switch self {
...
}
}
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.
Oh sorry, you're definitely right, I kind of did not realize it makes sense to have different ErrorType
for individual error cases 👍
case let .failedToDecrypt(reason): | ||
return "Could not decrypt data: \(reason)" | ||
case let .ivGenerationFailed(reason): | ||
return "Generation of IV failed with error: \(reason)" |
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 users do something to mitigate this issue? If so, what about including those steps here?
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 error should never occur - and honestly, if it does, I am not sure how I'd fix it 😞
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.
That's fine! If this error is very unlikely, we don't need to put a lot of effort into figuring out how to handle it. If it turns out to be more likely, then it's worth the investigation. Thanks for clarifying it!
let rootDirectory = rootDirectoryLocator.locate(from: path) | ||
else { throw SigningCipherError.signingDirectoryNotFound(path) } | ||
let signingDirectory = rootDirectory.appending(components: Constants.tuistDirectoryName, Constants.signingDirectoryName) | ||
let masterKey = try self.masterKey(from: rootDirectory) |
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'd require the key to be at /Tuist/master.key
@fortmarek I think this PR is in a good shape for a first iteration. There are some bits that I'd try to polish before making it official:
|
@pepibumur Thanks for your points! I agree that it needs some polish, before advertising I'd like to tackle how to use files in I agree with most of your points. Currently, the files are unencrypted in-place - that means we don't create new ones (so no files can be deleted either) - but I can see now that this might not be the ideal behavior since you could very easily mistakenly push unencrypted files to the remote repo. Would it make sense to encrypt files with But I'd still like to make the appropriate changes you suggested later - I think there might be more things that will need to be changed when I start working on integration with |
Short description 📝
This PR is part of signing feature described here. In this PR I want to tackle two steps: encryption and decryption, to be exact implementing two commands:
tuist signing encrypt
andtuist signing decrypt
Solution 📦
The basic logic for these commands is actually quite trivial: find
master.key
inTuist/Signing
directory and encrypt all the files in that directory and its subdirectories. The same process but vice versa is applied for decryption.I also needed to add some additional logic to support subcommands which SPM's parser does not out of the box.
Implementation 👩💻👨💻
When implementing this, I first used Apple's swift-crypto but unfortunately that requires macOS Catalina. I resorted to using CryptoSwift as a well-known alternative. We thought about using
openssl
and that'd create unnecessary overhead IMHO.