//
// SwiftyRSA.swift
// SwiftyRSA
//
// Created by Loïs Di Qual on 7/2/15.
//
// Copyright (c) 2015 Scoop Technologies, Inc. All rights reserved.
//
import Foundation
import Security
public typealias Padding = SecPadding
public enum SwiftyRSA {
static func base64String(pemEncoded pemString: String) throws -> String {
let lines = pemString.components(separatedBy: "\n").filter { line in
return !line.hasPrefix("-----BEGIN") && !line.hasPrefix("-----END")
}
guard lines.count != 0 else {
throw SwiftyRSAError.pemDoesNotContainKey
}
return lines.joined(separator: "")
}
static func isValidKeyReference(_ reference: SecKey, forClass requiredClass: CFString) -> Bool {
guard #available(iOS 10.0, *), #available(watchOS 3.0, *), #available(tvOS 10.0, *) else {
return true
}
let attributes = SecKeyCopyAttributes(reference) as? [CFString: Any]
guard let keyType = attributes?[kSecAttrKeyType] as? String, let keyClass = attributes?[kSecAttrKeyClass] as? String else {
return false
}
let isRSA = keyType == (kSecAttrKeyTypeRSA as String)
let isValidClass = keyClass == (requiredClass as String)
return isRSA && isValidClass
}
static func format(keyData: Data, withPemType pemType: String) -> String {
func split(_ str: String, byChunksOfLength length: Int) -> [String] {
return stride(from: 0, to: str.count, by: length).map { index -> String in
let startIndex = str.index(str.startIndex, offsetBy: index)
let endIndex = str.index(startIndex, offsetBy: length, limitedBy: str.endIndex) ?? str.endIndex
return String(str[startIndex..<endIndex])
}
}
// Line length is typically 64 characters, except the last line.
// See https://tools.ietf.org/html/rfc7468#page-6 (64base64char)
// See https://tools.ietf.org/html/rfc7468#page-11 (example)
let chunks = split(keyData.base64EncodedString(), byChunksOfLength: 64)
let pem = [
"-----BEGIN \(pemType)-----",
chunks.joined(separator: "\n"),
"-----END \(pemType)-----"
]
return pem.joined(separator: "\n")
}
static func data(forKeyReference reference: SecKey) throws -> Data {
// On iOS+, we can use `SecKeyCopyExternalRepresentation` directly
if #available(iOS 10.0, *), #available(watchOS 3.0, *), #available(tvOS 10.0, *) {
var error: Unmanaged<CFError>?
let data = SecKeyCopyExternalRepresentation(reference, &error)
guard let unwrappedData = data as Data? else {
throw SwiftyRSAError.keyRepresentationFailed(error: error?.takeRetainedValue())
}
return unwrappedData
// On iOS 8/9, we need to add the key again to the keychain with a temporary tag, grab the data,
// and delete the key again.
} else {
let temporaryTag = UUID().uuidString
let addParams: [CFString: Any] = [
kSecValueRef: reference,
kSecReturnData: true,
kSecClass: kSecClassKey,
kSecAttrApplicationTag: temporaryTag
]
var data: AnyObject?
let addStatus = SecItemAdd(addParams as CFDictionary, &data)
guard let unwrappedData = data as? Data else {
throw SwiftyRSAError.keyAddFailed(status: addStatus)
}
let deleteParams: [CFString: Any] = [
kSecClass: kSecClassKey,
kSecAttrApplicationTag: temporaryTag
]
_ = SecItemDelete(deleteParams as CFDictionary)
return unwrappedData
}
}
/// Will generate a new private and public key
///
/// - Parameters:
/// - size: Indicates the total number of bits in this cryptographic key
/// - Returns: A touple of a private and public key
/// - Throws: Throws and error if the tag cant be parsed or if keygeneration fails
@available(iOS 10.0, watchOS 3.0, tvOS 10.0, *)
public static func generateRSAKeyPair(sizeInBits size: Int) throws -> (privateKey: PrivateKey, publicKey: PublicKey) {
return try generateRSAKeyPair(sizeInBits: size, applyUnitTestWorkaround: false)
}
@available(iOS 10.0, watchOS 3.0, tvOS 10.0, *)
static func generateRSAKeyPair(sizeInBits size: Int, applyUnitTestWorkaround: Bool = false) throws -> (privateKey: PrivateKey, publicKey: PublicKey) {
guard let tagData = UUID().uuidString.data(using: .utf8) else {
throw SwiftyRSAError.stringToDataConversionFailed
}
// @hack Don't store permanently when running unit tests, otherwise we'll get a key creation error (NSOSStatusErrorDomain -50)
// @see http://www.openradar.me/36809637
// @see https://stackoverflow.com/q/48414685/646960
let isPermanent = applyUnitTestWorkaround ? false : true
let attributes: [CFString: Any] = [
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits: size,
kSecPrivateKeyAttrs: [
kSecAttrIsPermanent: isPermanent,
kSecAttrApplicationTag: tagData
]
]
var error: Unmanaged<CFError>?
guard let privKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error),
let pubKey = SecKeyCopyPublicKey(privKey) else {
throw SwiftyRSAError.keyGenerationFailed(error: error?.takeRetainedValue())
}
let privateKey = try PrivateKey(reference: privKey)
let publicKey = try PublicKey(reference: pubKey)
return (privateKey: privateKey, publicKey: publicKey)
}
static func addKey(_ keyData: Data, isPublic: Bool, tag: String) throws -> SecKey {
let keyData = keyData
guard let tagData = tag.data(using: .utf8) else {
throw SwiftyRSAError.tagEncodingFailed
}
let keyClass = isPublic ? kSecAttrKeyClassPublic : kSecAttrKeyClassPrivate
// On iOS 10+, we can use SecKeyCreateWithData without going through the keychain
if #available(iOS 10.0, *), #available(watchOS 3.0, *), #available(tvOS 10.0, *) {
let sizeInBits = keyData.count * 8
let keyDict: [CFString: Any] = [
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeyClass: keyClass,
kSecAttrKeySizeInBits: NSNumber(value: sizeInBits),
kSecReturnPersistentRef: true
]
var error: Unmanaged<CFError>?
guard let key = SecKeyCreateWithData(keyData as CFData, keyDict as CFDictionary, &error) else {
throw SwiftyRSAError.keyCreateFailed(error: error?.takeRetainedValue())
}
return key
// On iOS 9 and earlier, add a persistent version of the key to the system keychain
} else {
let persistKey = UnsafeMutablePointer<AnyObject?>(mutating: nil)
let keyAddDict: [CFString: Any] = [
kSecClass: kSecClassKey,
kSecAttrApplicationTag: tagData,
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecValueData: keyData,
kSecAttrKeyClass: keyClass,
kSecReturnPersistentRef: true,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock
]
let addStatus = SecItemAdd(keyAddDict as CFDictionary, persistKey)
guard addStatus == errSecSuccess || addStatus == errSecDuplicateItem else {
throw SwiftyRSAError.keyAddFailed(status: addStatus)
}
let keyCopyDict: [CFString: Any] = [
kSecClass: kSecClassKey,
kSecAttrApplicationTag: tagData,
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeyClass: keyClass,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
kSecReturnRef: true,
]
// Now fetch the SecKeyRef version of the key
var keyRef: AnyObject?
let copyStatus = SecItemCopyMatching(keyCopyDict as CFDictionary, &keyRef)
guard let unwrappedKeyRef = keyRef else {
throw SwiftyRSAError.keyCopyFailed(status: copyStatus)
}
return unwrappedKeyRef as! SecKey // swiftlint:disable:this force_cast
}
}
/**
This method strips the x509 header from a provided ASN.1 DER key.
If the key doesn't contain a header, the DER data is returned as is.
Supported formats are:
Headerless:
SEQUENCE
INTEGER (1024 or 2048 bit) -- modulo
INTEGER -- public exponent
With x509 header:
SEQUENCE
SEQUENCE
OBJECT IDENTIFIER 1.2.840.113549.1.1.1
NULL
BIT STRING
SEQUENCE
INTEGER (1024 or 2048 bit) -- modulo
INTEGER -- public exponent
Example of headerless key:
https://lapo.it/asn1js/#3082010A0282010100C1A0DFA367FBC2A5FD6ED5A071E02A4B0617E19C6B5AD11BB61192E78D212F10A7620084A3CED660894134D4E475BAD7786FA1D40878683FD1B7A1AD9C0542B7A666457A270159DAC40CE25B2EAE7CCD807D31AE725CA394F90FBB5C5BA500545B99C545A9FE08EFF00A5F23457633E1DB84ED5E908EF748A90F8DFCCAFF319CB0334705EA012AF15AA090D17A9330159C9AFC9275C610BB9B7C61317876DC7386C723885C100F774C19830F475AD1E9A9925F9CA9A69CE0181A214DF2EB75FD13E6A546B8C8ED699E33A8521242B7E42711066AEC22D25DD45D56F94D3170D6F2C25164D2DACED31C73963BA885ADCB706F40866B8266433ED5161DC50E4B3B0203010001
Example of key with X509 header (notice the additional ASN.1 sequence):
https://lapo.it/asn1js/#30819F300D06092A864886F70D010101050003818D0030818902818100D0674615A252ED3D75D2A3073A0A8A445F3188FD3BEB8BA8584F7299E391BDEC3427F287327414174997D147DD8CA62647427D73C9DA5504E0A3EED5274A1D50A1237D688486FADB8B82061675ABFA5E55B624095DB8790C6DBCAE83D6A8588C9A6635D7CF257ED1EDE18F04217D37908FD0CBB86B2C58D5F762E6207FF7B92D0203010001
*/
static func stripKeyHeader(keyData: Data) throws -> Data {
let node: Asn1Parser.Node
do {
node = try Asn1Parser.parse(data: keyData)
} catch {
throw SwiftyRSAError.asn1ParsingFailed
}
// Ensure the raw data is an ASN1 sequence
guard case .sequence(let nodes) = node else {
throw SwiftyRSAError.invalidAsn1RootNode
}
// Detect whether the sequence only has integers, in which case it's a headerless key
let onlyHasIntegers = nodes.filter { node -> Bool in
if case .integer = node {
return false
}
return true
}.isEmpty
// Headerless key
if onlyHasIntegers {
return keyData
}
// If last element of the sequence is a bit string, return its data
if let last = nodes.last, case .bitString(let data) = last {
return data
}
// If last element of the sequence is an octet string, return its data
if let last = nodes.last, case .octetString(let data) = last {
return data
}
// Unable to extract bit/octet string or raw integer sequence
throw SwiftyRSAError.invalidAsn1Structure
}
/**
This method prepend the x509 header to the given PublicKey data.
If the key already contain a x509 header, the given data is returned as is.
It letterally does the opposite of the previous method :
From a given headerless key :
SEQUENCE
INTEGER (1024 or 2048 bit) -- modulo
INTEGER -- public exponent
the key is returned following the X509 header :
SEQUENCE
SEQUENCE
OBJECT IDENTIFIER 1.2.840.113549.1.1.1
NULL
BIT STRING
SEQUENCE
INTEGER (1024 or 2048 bit) -- modulo
INTEGER -- public exponent
*/
static func prependX509KeyHeader(keyData: Data) throws -> Data {
if try keyData.isAnHeaderlessKey() {
let x509certificate: Data = keyData.prependx509Header()
return x509certificate
} else if try keyData.hasX509Header() {
return keyData
} else { // invalideHeader
throw SwiftyRSAError.x509CertificateFailed
}
}
static func removeKey(tag: String) {
guard let tagData = tag.data(using: .utf8) else {
return
}
let keyRemoveDict: [CFString: Any] = [
kSecClass: kSecClassKey,
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrApplicationTag: tagData,
]
SecItemDelete(keyRemoveDict as CFDictionary)
}
}
#if !swift(>=4.1)
extension Array {
func compactMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] {
return try self.flatMap(transform)
}
}
#endif
#if !swift(>=4.0)
extension NSTextCheckingResult {
func range(at idx: Int) -> NSRange {
return self.rangeAt(1)
}
}
#endif