174 lines
5.6 KiB
Swift
174 lines
5.6 KiB
Swift
import Foundation
|
|
|
|
let version = "v0.0.3"
|
|
|
|
@main
|
|
struct CLI {
|
|
static func main() {
|
|
do {
|
|
let plugin = Plugin(crypto: CryptoKitCrypto(), stream: StandardIOStream())
|
|
let options = try Options.parse(CommandLine.arguments)
|
|
switch options.command {
|
|
case .help:
|
|
print(Options.help)
|
|
case .version:
|
|
print(version)
|
|
case .keygen:
|
|
let result = try plugin.generateKey(
|
|
accessControl: options.accessControl.keyAccessControl, now: Date())
|
|
if let outputFile = options.output {
|
|
FileManager.default.createFile(
|
|
atPath: FileManager.default.currentDirectoryPath + "/" + outputFile,
|
|
contents: result.0.data(using: .utf8),
|
|
attributes: [.posixPermissions: 0o600]
|
|
)
|
|
print("Public key: \(result.1)")
|
|
} else {
|
|
print(result.0)
|
|
}
|
|
case .plugin(let sm):
|
|
switch sm {
|
|
case .recipientV1:
|
|
plugin.runRecipientV1()
|
|
case .identityV1:
|
|
plugin.runIdentityV1()
|
|
}
|
|
}
|
|
} catch {
|
|
print("\(CommandLine.arguments[0]): error: \(error.localizedDescription)")
|
|
exit(-1)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Command-line options parser
|
|
struct Options {
|
|
enum Error: LocalizedError, Equatable {
|
|
case unknownOption(String)
|
|
case missingValue(String)
|
|
case invalidValue(String, String)
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .unknownOption(let option): return "unknown option: `\(option)`"
|
|
case .missingValue(let option): return "missing value for option `\(option)`"
|
|
case .invalidValue(let option, let value):
|
|
return "invalid value for option `\(option)`: `\(value)`"
|
|
}
|
|
}
|
|
}
|
|
|
|
enum StateMachine: String {
|
|
case recipientV1 = "recipient-v1"
|
|
case identityV1 = "identity-v1"
|
|
}
|
|
|
|
enum Command: Equatable {
|
|
case help
|
|
case version
|
|
case keygen
|
|
case plugin(StateMachine)
|
|
}
|
|
var command: Command
|
|
|
|
var output: String?
|
|
|
|
enum AccessControl: String {
|
|
case none = "none"
|
|
case passcode = "passcode"
|
|
case anyBiometry = "any-biometry"
|
|
case anyBiometryOrPasscode = "any-biometry-or-passcode"
|
|
case anyBiometryAndPasscode = "any-biometry-and-passcode"
|
|
case currentBiometry = "current-biometry"
|
|
case currentBiometryAndPasscode = "current-biometry-and-passcode"
|
|
|
|
var keyAccessControl: KeyAccessControl {
|
|
switch self {
|
|
case .none: return KeyAccessControl.none
|
|
case .passcode: return KeyAccessControl.passcode
|
|
case .anyBiometry: return KeyAccessControl.anyBiometry
|
|
case .anyBiometryOrPasscode: return KeyAccessControl.anyBiometryOrPasscode
|
|
case .anyBiometryAndPasscode: return KeyAccessControl.anyBiometryAndPasscode
|
|
case .currentBiometry: return KeyAccessControl.currentBiometry
|
|
case .currentBiometryAndPasscode: return KeyAccessControl.currentBiometryAndPasscode
|
|
}
|
|
}
|
|
}
|
|
var accessControl = AccessControl.anyBiometryOrPasscode
|
|
|
|
static var help =
|
|
"""
|
|
Usage:
|
|
age-plugin-se keygen [-o OUTPUT] [--access-control ACCESS_CONTROL]
|
|
|
|
Options:
|
|
-o, --output OUTPUT Write the result to the file at path OUTPUT
|
|
|
|
--access-control ACCESS_CONTROL Access control for using the generated key.
|
|
|
|
When using current biometry, adding or removing a fingerprint stops the
|
|
key from working. Removing an added fingerprint enables the key again.
|
|
|
|
Supported values: none, passcode,
|
|
any-biometry, any-biometry-and-passcode, any-biometry-or-passcode,
|
|
current-biometry, current-biometry-and-passcode
|
|
Default: any-biometry-or-passcode.
|
|
|
|
Example:
|
|
$ age-plugin-se keygen -o key.txt
|
|
Public key: age1se1qg8vwwqhztnh3vpt2nf2xwn7famktxlmp0nmkfltp8lkvzp8nafkqleh258
|
|
$ tar cvz ~/data | age -r age1se1qgg72x2qfk9wg3wh0qg9u0v7l5dkq4jx69fv80p6wdus3ftg6flwg5dz2dp > data.tar.gz.age
|
|
$ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz
|
|
"""
|
|
|
|
static func parse(_ args: [String]) throws -> Options {
|
|
var opts = Options(command: .help)
|
|
var i = 1
|
|
while i < args.count {
|
|
let arg = args[i]
|
|
if arg == "keygen" {
|
|
opts.command = .keygen
|
|
} else if ["--help", "-h"].contains(arg) {
|
|
opts.command = .help
|
|
break
|
|
} else if ["--version"].contains(arg) {
|
|
opts.command = .version
|
|
break
|
|
} else if [
|
|
"--age-plugin", "-o", "--output", "--access-control",
|
|
].contains(where: {
|
|
arg == $0 || arg.hasPrefix($0 + "=")
|
|
}) {
|
|
let argps = arg.split(separator: "=", maxSplits: 1)
|
|
let value: String
|
|
if argps.count == 1 {
|
|
i += 1
|
|
if i >= args.count {
|
|
throw Error.missingValue(arg)
|
|
}
|
|
value = args[i]
|
|
} else {
|
|
value = String(argps[1])
|
|
}
|
|
let arg = String(argps[0])
|
|
switch arg {
|
|
case "--age-plugin":
|
|
opts.command = try .plugin(
|
|
StateMachine(rawValue: value) ?? { throw Error.invalidValue(arg, value) }())
|
|
case "-o", "--output":
|
|
opts.output = value
|
|
case "--access-control":
|
|
opts.accessControl =
|
|
try AccessControl(rawValue: value) ?? { throw Error.invalidValue(arg, value) }()
|
|
default:
|
|
assert(false)
|
|
}
|
|
} else {
|
|
throw Error.unknownOption(arg)
|
|
}
|
|
i += 1
|
|
}
|
|
return opts
|
|
}
|
|
}
|