// // 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