Skip to content

Commit

Permalink
Add ability to generate RSA key pairs with ‘SwiftyRSA.generateRSAKeyP…
Browse files Browse the repository at this point in the history
…air’ (#116)

* Added ‘generateRSAKeyPair’ to SwiftyRSA enum.

The method was added together with an accompanying test, although the test will fail due to what is believed to be a unit test bug http://www.openradar.me/36809637

Signed-off-by: Christoffer Buusmann <[email protected]>

* Added further documentation

Signed-off-by: Christoffer Buusmann <[email protected]>

* .

* Access level and linting

Added ‘public’ access level to SwiftRSA enum and generateRSAKeyPair static func. Did some linting

* Updated changelog and readme for the new generateRSAKeyPair method

* Revert "."

This reverts commit 5f2fe52.

* Changed whitespace

* Refactored keygen method and associated test

Cleaned up function parameters for key generation and made the attributes dict of type CFString:Any to remove messy downcasts.

* Linting changes and typo fix. Changelog corrected

* Fix changelog + readme

* Add unit test workaround for key generation

* Revert scheme

* More availability tags

* Bump to Xcode 9.2, disable test builds for iOS runs

* Revert iOS scheme

* Keep trying carthage

* Add shared workspace settings
  • Loading branch information
ldiqual authored Apr 30, 2018
1 parent a6ff30c commit 2631d44
Show file tree
Hide file tree
Showing 12 changed files with 122 additions and 23 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ SwiftyRSA Changelog
# [master]

- Fixed compilation warnings for Xcode 9.1 / 9.2.
- Added ability to generate a RSA key pair by using `SwiftyRSA.generateRSAKeyPair`.
[#106](https://github.com/TakeScoop/SwiftyRSA/issues/106)

# [1.3.0]

Expand Down
1 change: 1 addition & 0 deletions CarthageIntegrationTest/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# We're ignoring the .resolved file so Carthage always fetches master during Travis CI tests
Cartfile
Cartfile.resolved
1 change: 0 additions & 1 deletion CarthageIntegrationTest/Cartfile

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ let signature = try Signature(base64Encoded: "AAA===")
let isSuccessful = try clear.verify(with: publicKey, signature: signature, digestType: .sha1)
```

### Create a public/private RSA key pair

```swift
let keyPair = SwiftRSA.generateRSAKeyPair(sizeInBits: 2048)
let privateKey = keyPair.privateKey
let publicKey = keyPair.publicKey
```

### Export a key or access its content

```swift
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C076F53C1DADEB90006AF5DB"
Expand All @@ -40,8 +40,8 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
codeCoverageEnabled = "YES"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
Expand Down
63 changes: 53 additions & 10 deletions SwiftyRSA/SwiftyRSA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ extension Data {
}
}

enum SwiftyRSA {
public enum SwiftyRSA {

static func base64String(pemEncoded pemString: String) throws -> String {
let lines = pemString.components(separatedBy: "\n").filter { line in
Expand Down Expand Up @@ -122,6 +122,49 @@ enum SwiftyRSA {
}
}

/// 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, *) @available(watchOS 3.0, *) @available(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, *) @available(watchOS 3.0, *) @available(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 {

var keyData = keyData
Expand Down Expand Up @@ -198,18 +241,18 @@ enum SwiftyRSA {

Headerless:
SEQUENCE
INTEGER (1024 or 2048 bit) -- modulo
INTEGER -- public exponent
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
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
Expand Down
3 changes: 3 additions & 0 deletions SwiftyRSA/SwiftyRSAError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum SwiftyRSAError: Error {

case pemDoesNotContainKey
case keyRepresentationFailed(error: CFError?)
case keyGenerationFailed(error: CFError?)
case keyCreateFailed(error: CFError?)
case keyAddFailed(status: OSStatus)
case keyCopyFailed(status: OSStatus)
Expand All @@ -38,6 +39,8 @@ public enum SwiftyRSAError: Error {
return "Couldn't get data from PEM key: no data available after stripping headers"
case .keyRepresentationFailed(let error):
return "Couldn't retrieve key data from the keychain: CFError \(String(describing: error))"
case .keyGenerationFailed(let error):
return "Couldn't generate key pair: CFError: \(String(describing: error))"
case .keyCreateFailed(let error):
return "Couldn't create key reference from key data: CFError \(String(describing: error))"
case .keyAddFailed(let status):
Expand Down
30 changes: 28 additions & 2 deletions SwiftyRSATests/KeyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
//

import XCTest
import SwiftyRSA

// Using @testable here so we can call `SwiftyRSA.generateRSAKeyPair(sizeInBits:applyUnitTestWorkaround)`
@testable import SwiftyRSA

class PublicKeyTests: XCTestCase {

Expand All @@ -19,7 +21,6 @@ class PublicKeyTests: XCTestCase {
}
let data = try Data(contentsOf: URL(fileURLWithPath: path))
let publicKey = try PublicKey(data: data)

let newPublicKey = try? PublicKey(reference: publicKey.reference)
XCTAssertNotNil(newPublicKey)
}
Expand Down Expand Up @@ -249,4 +250,29 @@ class PrivateKeyTests: XCTestCase {
func test_headerAndOctetString() throws {
_ = try PrivateKey(pemNamed: "swiftyrsa-private-header-octetstring", in: bundle)
}

@available(iOS 10.0, *) @available(watchOS 3.0, *) @available(tvOS 10.0, *)
func test_generateKeyPair() throws {

let keyPair = try SwiftyRSA.generateRSAKeyPair(sizeInBits: 2048, applyUnitTestWorkaround: true)

let algorithm: SecKeyAlgorithm = .rsaEncryptionOAEPSHA512
guard SecKeyIsAlgorithmSupported(keyPair.privateKey.reference, .decrypt, algorithm) else {
XCTFail("Key cannot be used for decryption")
return
}

guard SecKeyIsAlgorithmSupported(keyPair.publicKey.reference, .encrypt, algorithm) else {
XCTFail("Key cannot be used for encryption")
return
}

let str = "Clear Text"
let clearMessage = try ClearMessage(string: str, using: .utf8)

let encrypted = try clearMessage.encrypted(with: keyPair.publicKey, padding: .PKCS1)
let decrypted = try encrypted.decrypted(with: keyPair.privateKey, padding: .PKCS1)

XCTAssertEqual(try? decrypted.string(encoding: .utf8), str)
}
}
2 changes: 1 addition & 1 deletion circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ machine:
pre:
- brew install ruby; brew link --overwrite ruby
xcode:
version: "9.0"
version: "9.2.0"
environment:
SCAN_OUTPUT_DIRECTORY: $CIRCLE_TEST_REPORTS/junit
dependencies:
Expand Down
11 changes: 6 additions & 5 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ platform :ios do
lane :test_carthage do
commit = last_git_commit()[:commit_hash]
git_repo_path = File.expand_path('..', File.dirname(__FILE__))
sh "rm ../CarthageIntegrationTest/Cartfile* || true"
sh "echo 'git \"file://#{git_repo_path}\" \"#{commit}\"' > ../CarthageIntegrationTest/Cartfile"
carthage(project_directory: './CarthageIntegrationTest')
xcodebuild(project: './CarthageIntegrationTest/CarthageIntegrationTest.xcodeproj')
Expand All @@ -27,7 +28,7 @@ platform :ios do
# iOS
scan(
scheme: 'SwiftyRSA iOS',
sdk: 'iphonesimulator11.0',
sdk: 'iphonesimulator11.2',
output_types: 'junit',
output_files: 'iOS.xml',
xcargs: "SWIFT_VERSION='#{swift_version}'",
Expand All @@ -37,14 +38,14 @@ platform :ios do

# Disabled because of https://github.com/fastlane/fastlane/issues/10338
# 'iPhone 6 (10.3.1)',
# 'iPhone 6 (11.0)'
# 'iPhone 6 (11.2)'
]
)

# tvOS
scan(
scheme: 'SwiftyRSA tvOS',
sdk: 'appletvsimulator11.0',
sdk: 'appletvsimulator11.2',
output_types: 'junit',
output_files: 'tvOS.xml',
xcargs: "SWIFT_VERSION='#{swift_version}'",
Expand All @@ -54,14 +55,14 @@ platform :ios do

# Disabled because of https://github.com/fastlane/fastlane/issues/10338
# 'Apple TV 1080p (10.3.1)',
# 'Apple TV 1080p (11.0)',
# 'Apple TV 1080p (11.2)',
]
)

# watchOS (smoke test)
xcodebuild(
scheme: 'SwiftyRSA watchOS',
sdk: 'watchsimulator4.0',
sdk: 'watchsimulator4.2',
configuration: 'Debug',
xcargs: "SWIFT_VERSION='#{swift_version}'"
)
Expand Down

0 comments on commit 2631d44

Please sign in to comment.