import Foundation
extension Data {
init?(base64RawEncoded: String) {
if base64RawEncoded.hasSuffix("="){
return nil
}
var str = base64RawEncoded
switch base64RawEncoded.count % 4 {
case 2:
str += "=="case 3:
str += "="default:
()
}
guard let data = Data(base64Encoded: str) else {
return nil
}
self = data
}
var base64RawEncodedData: Data {
var s = base64EncodedData(options: [
Base64EncodingOptions.lineLength64Characters, Base64EncodingOptions.endLineWithLineFeed,
])
if let pi = s.firstIndex(of: Character("=").asciiValue!) {
s = Data(s[s.startIndex..<pi])
}
return s
}
var base64RawEncodedString: String {
return String(decoding: base64RawEncodedData, as: UTF8.self)
}
}
// Spec: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
// Modified version of https://github.com/0xDEADP00L/Bech32
// Copyright 2018 Evolution Group Limited
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import Foundation
/// Bech32 checksum implementation
public class Bech32 {
private let gen: [UInt32] = [0x3b6a_57b2, 0x2650_8e6d, 0x1ea1_19fa, 0x3d42_33dd, 0x2a14_62b3]
/// Bech32 checksum delimiter
private let checksumMarker: String = "1"
/// Bech32 character set for encoding
private let encCharset: Data = Data("qpzry9x8gf2tvdw0s3jn54khce6mua7l".utf8)
/// Bech32 character set for decoding
private let decCharset: [Int8] = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1,
]
private func convertBits(from: Int, to: Int, pad: Bool, idata: Data) throws -> Data {
var acc: Int = 0
var bits: Int = 0
let maxv: Int = (1 << to) - 1
let maxAcc: Int = (1 << (from + to - 1)) - 1
var odata = Data()
for ibyte in idata {
acc = ((acc << from) | Int(ibyte)) & maxAcc
bits += from
while bits >= to{
bits -= to
odata.append(UInt8((acc >> bits) & maxv))
}
}
if pad{
if bits != 0{
odata.append(UInt8((acc << (to - bits)) & maxv))
}
} else if bits >= from || ((acc << (to - bits)) & maxv) != 0{
throw DecodingError.bitsConversionFailed
}
return odata
}
/// Find the polynomial with value coefficients mod the generator as 30-bit.
private func polymod(_ values: Data) -> UInt32 {
var chk: UInt32 = 1
for v in values {
let top = (chk >> 25)
chk = (chk & 0x1ffffff) << 5 ^ UInt32(v)
for i: UInt8 in 0..<5 {
chk ^= ((top >> i) & 1) == 0 ? 0 : gen[Int(i)]
}
}
return chk
}
/// Expand a HRP for use in checksum computation.
private func expandHrp(_ hrp: String) -> Data {
let hrpBytes = Data(hrp.utf8)
var result = Data(repeating: 0x00, count: hrpBytes.count * 2 + 1)
for (i, c) in hrpBytes.enumerated() {
result[i] = c >> 5
result[i + hrpBytes.count + 1] = c & 0x1f
}
result[hrp.count] = 0
return result
}
/// Verify checksum
private func verifyChecksum(hrp: String, checksum: Data) -> Bool {
var data = expandHrp(hrp)
data.append(checksum)
return polymod(data) == 1
}
/// Create checksum
private func createChecksum(hrp: String, values: Data) -> Data {
var enc = expandHrp(hrp)
enc.append(values)
enc.append(Data(repeating: 0x00, count: 6))
let mod: UInt32 = polymod(enc) ^ 1
var ret: Data = Data(repeating: 0x00, count: 6)
for i in 0..<6 {
ret[i] = UInt8((mod >> (5 * (5 - i))) & 31)
}
return ret
}
/// Encode Bech32 string
private func encodeBech32(_ hrp: String, values: Data) -> String {
let checksum = createChecksum(hrp: hrp, values: values)
var combined = values
combined.append(checksum)
let hrpBytes = Data(hrp.utf8)
var ret = hrpBytes
ret.append(Data("1".utf8))
for i in combined {
ret.append(encCharset[Int(i)])
}
return String(decoding: ret, as: UTF8.self)
}
/// Decode Bech32 string
public func decodeBech32(_ str: String) throws -> (hrp: String, checksum: Data) {
let strBytes = Data(str.utf8)
var lower: Bool = false
var upper: Bool = false
for c in strBytes {
// printable range
if c < 33 || c > 126{
throw DecodingError.nonPrintableCharacter
}
// 'a' to 'z'
if c >= 97 && c <= 122{
lower = true
}
// 'A' to 'Z'
if c >= 65 && c <= 90{
upper = true
}
}
if lower && upper{
throw DecodingError.invalidCase
}
guard let pos = str.range(of: checksumMarker, options: .backwards)?.lowerBound else {
throw DecodingError.noChecksumMarker
}
let intPos: Int = str.distance(from: str.startIndex, to: pos)
guard intPos >= 1 else {
throw DecodingError.incorrectHrpSize
}
guard intPos + 7 <= str.count else {
throw DecodingError.incorrectChecksumSize
}
let vSize: Int = str.count - 1 - intPos
var values: Data = Data(repeating: 0x00, count: vSize)
for i in 0..<vSize {
let c = strBytes[i + intPos + 1]
let decInt = decCharset[Int(c)]
if decInt == -1{
throw DecodingError.invalidCharacter
}
values[i] = UInt8(decInt)
}
let hrp = String(str[..<pos]).lowercased()
guard verifyChecksum(hrp: hrp, checksum: values) else {
throw DecodingError.checksumMismatch
}
return (hrp, Data(values[..<(vSize - 6)]))
}
public func encode(hrp: String, data: Data) -> String {
let isUpper = hrp[hrp.startIndex].isUppercase
let result = encodeBech32(
isUpper ? hrp.lowercased() : hrp,
values: try! self.convertBits(from: 8, to: 5, pad: true, idata: data))
return isUpper ? result.uppercased() : result
}
public func decode(_ str: String) throws -> (hrp: String, data: Data) {
let isUpper = str[str.startIndex].isUppercase
let result = try decodeBech32(isUpper ? str.lowercased() : str)
return (
isUpper ? result.hrp.uppercased() : result.hrp,
try convertBits(from: 5, to: 8, pad: false, idata: result.checksum)
)
}
}
extension Bech32 {
public enum DecodingError: LocalizedError {
case nonUTF8String
case nonPrintableCharacter
case invalidCase
case noChecksumMarker
case incorrectHrpSize
case incorrectChecksumSize
case invalidCharacter
case checksumMismatch
case bitsConversionFailed
public var errorDescription: String? {
switch self {
case .bitsConversionFailed:
return "Failed to perform bits conversion"case .checksumMismatch:
return "Checksum doesn't match"case .incorrectChecksumSize:
return "Checksum size too low"case .incorrectHrpSize:
return "Human-readable-part is too small or empty"case .invalidCase:
return "String contains mixed case characters"case .invalidCharacter:
return "Invalid character met on decoding"case .noChecksumMarker:
return "Checksum delimiter not found"case .nonPrintableCharacter:
return "Non printable character in input string"case .nonUTF8String:
return "String cannot be decoded by utf8 decoder"
}
}
}
}
import Foundation
let version = "v0.1.4"
@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,
recipientType: options.recipientType.recipientType, now: Date())
if let outputFile = options.output {
FileManager.default.createFile(
atPath: outputFile,
contents: Data(result.0.utf8),
attributes: [.posixPermissions: 0o600]
)
print("Public key: \(result.1)")
} else {
print(result.0)
}case .recipients:
var input = ""
if let inputFile = options.input {
input = try String(
contentsOfFile: inputFile)
} else {
input = try String(data: FileHandle.standardInput.readToEnd()!, encoding: .utf8)!
}
let result = try plugin.generateRecipients(
input: input, recipientType: options.recipientType.recipientType)
if let outputFile = options.output {
FileManager.default.createFile(
atPath: outputFile,
contents: Data(result.utf8),
attributes: [.posixPermissions: 0o600]
)
} else if result != ""{
print(result)
}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 recipients
case plugin(StateMachine)
}
var command: Command
var output: String?
var input: 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.nonecase .passcode: return KeyAccessControl.passcodecase .anyBiometry: return KeyAccessControl.anyBiometrycase .anyBiometryOrPasscode: return KeyAccessControl.anyBiometryOrPasscodecase .anyBiometryAndPasscode: return KeyAccessControl.anyBiometryAndPasscodecase .currentBiometry: return KeyAccessControl.currentBiometrycase .currentBiometryAndPasscode: return KeyAccessControl.currentBiometryAndPasscode
}
}
}
var accessControl = AccessControl.anyBiometryOrPasscode
enum RecipientType: String {
case se = "se"
case p256tag = "p256tag"
var recipientType: age_plugin_se.RecipientType {
switch self {
case .se: return .secase .p256tag: return .p256tag
}
}
}
var recipientType: RecipientType = .se
static let help =
"""
Usage:
age-plugin-se keygen [-o OUTPUT] [--access-control ACCESS_CONTROL]
age-plugin-se recipients [-o OUTPUT] [-i INPUT]
Description:
The `keygen` subcommand generates a new private key bound to the current
Secure Enclave, with the given access controls, and outputs it to OUTPUT
or standard output.
The `recipients` subcommand reads an identity file from INPUT or standard
input, and outputs the corresponding recipient(s) to OUTPUT or to standard
output.
Options:
--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.
-i, --input INPUT Read data from the file at path INPUT
-o, --output OUTPUT Write the result to the file at path OUTPUT
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 arg == "recipients"{
opts.command = .recipients
} else if ["--help", "-h"].contains(arg){
opts.command = .help
break
} else if ["--version"].contains(arg){
opts.command = .version
break
} else if [
"--age-plugin", "-i", "--input", "-o", "--output", "--access-control", "--recipient-type",
].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 "-i", "--input":
opts.input = valuecase "-o", "--output":
opts.output = valuecase "--access-control":
opts.accessControl =
try AccessControl(rawValue: value) ?? { throw Error.invalidValue(arg, value) }()case "--recipient-type":
opts.recipientType =
try RecipientType(rawValue: value) ?? { throw Error.invalidValue(arg, value) }()default:
assert(false)
}
} else {
throw Error.unknownOption(arg)
}
i += 1
}
return opts
}
}