Skip to content

Commit ece768c

Browse files
committed
Initial commit
0 parents  commit ece768c

File tree

11 files changed

+388
-0
lines changed

11 files changed

+388
-0
lines changed

.gitattributes

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto

.gitignore

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Xcode
2+
#
3+
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4+
5+
## User settings
6+
xcuserdata/
7+
8+
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9+
*.xcscmblueprint
10+
*.xccheckout
11+
12+
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13+
build/
14+
DerivedData/
15+
*.moved-aside
16+
*.pbxuser
17+
!default.pbxuser
18+
*.mode1v3
19+
!default.mode1v3
20+
*.mode2v3
21+
!default.mode2v3
22+
*.perspectivev3
23+
!default.perspectivev3
24+
25+
## Obj-C/Swift specific
26+
*.hmap
27+
28+
## App packaging
29+
*.ipa
30+
*.dSYM.zip
31+
*.dSYM
32+
33+
## Playgrounds
34+
timeline.xctimeline
35+
playground.xcworkspace
36+
37+
# Swift Package Manager
38+
#
39+
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40+
# Packages/
41+
# Package.pins
42+
# Package.resolved
43+
# *.xcodeproj
44+
#
45+
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46+
# hence it is not needed unless you have added a package configuration file to your project
47+
# .swiftpm
48+
49+
.build/
50+
51+
# CocoaPods
52+
#
53+
# We recommend against adding the Pods directory to your .gitignore. However
54+
# you should judge for yourself, the pros and cons are mentioned at:
55+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56+
#
57+
# Pods/
58+
#
59+
# Add this line if you want to avoid checking in source code from the Xcode workspace
60+
# *.xcworkspace
61+
62+
# Carthage
63+
#
64+
# Add this line if you want to avoid checking in source code from Carthage dependencies.
65+
# Carthage/Checkouts
66+
67+
Carthage/Build/
68+
69+
# Accio dependency management
70+
Dependencies/
71+
.accio/
72+
73+
# fastlane
74+
#
75+
# It is recommended to not store the screenshots in the git repo.
76+
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
77+
# For more information about the recommended setup visit:
78+
# https://docs.fastlane.tools/best-practices/source-control/#source-control
79+
80+
fastlane/report.xml
81+
fastlane/Preview.html
82+
fastlane/screenshots/**/*.png
83+
fastlane/test_output
84+
85+
# Code Injection
86+
#
87+
# After new code Injection tools there's a generated folder /iOSInjectionProject
88+
# https://github.com/johnno1962/injectionforxcode
89+
90+
iOSInjectionProject/

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 consuelita
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Package.swift

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// swift-tools-version:5.3
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "Keychain",
8+
platforms: [.macOS(.v10_15)],
9+
products: [
10+
// Products define the executables and libraries a package produces, and make them visible to other packages.
11+
.library(
12+
name: "Keychain",
13+
targets: ["Keychain"]),
14+
],
15+
dependencies: [
16+
// Dependencies declare other packages that this package depends on.
17+
// .package(url: /* package url */, from: "1.0.0"),
18+
],
19+
targets: [
20+
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
21+
// Targets can depend on other targets in this package, and on products in packages this package depends on.
22+
.target(
23+
name: "Keychain",
24+
dependencies: []),
25+
.testTarget(
26+
name: "KeychainTests",
27+
dependencies: ["Keychain"]),
28+
],
29+
swiftLanguageVersions: [.v5]
30+
)

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Keychain
2+
Simple keychain wrapper for P256 keys for macOS and iOS.

Sources/Keychain/Keychain.swift

+198
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
2+
import CryptoKit
3+
import Foundation
4+
5+
6+
class Keychain
7+
{
8+
func retrieveOrGeneratePrivateKey(label: String, tag: String) -> P256.KeyAgreement.PrivateKey?
9+
{
10+
// Do we already have a key?
11+
let searchQuery = generateKeySearchQuery(label: label, tag: tag)
12+
if let key = retrievePrivateKey(query: searchQuery)
13+
{
14+
return key
15+
}
16+
17+
// We don't?
18+
// Let's create some and return those
19+
let privateKey = P256.KeyAgreement.PrivateKey()
20+
21+
// Save the key we stored
22+
let stored = storePrivateKey(privateKey, label: label)
23+
if !stored
24+
{
25+
print("😱 Failed to store our new server key.")
26+
return nil
27+
}
28+
return privateKey
29+
}
30+
31+
func generateAndSavePrivateKey(label: String) -> P256.KeyAgreement.PrivateKey?
32+
{
33+
let privateKey = P256.KeyAgreement.PrivateKey()
34+
35+
// Save the key we stored
36+
let stored = storePrivateKey(privateKey, label: label)
37+
if !stored
38+
{
39+
print("😱 Failed to store our new server key.")
40+
return nil
41+
}
42+
43+
return privateKey
44+
}
45+
46+
func storePrivateKey(_ key: P256.KeyAgreement.PrivateKey, label: String) -> Bool
47+
{
48+
let attributes = [kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
49+
kSecAttrKeyClass: kSecAttrKeyClassPrivate] as [String: Any]
50+
51+
// Get a SecKey representation.
52+
var error: Unmanaged<CFError>?
53+
let keyData = key.x963Representation as CFData
54+
guard let secKey = SecKeyCreateWithData(keyData,
55+
attributes as CFDictionary,
56+
&error)
57+
else
58+
{
59+
print("Unable to create SecKey representation.")
60+
if let secKeyError = error
61+
{
62+
print(secKeyError)
63+
}
64+
return false
65+
}
66+
67+
// Describe the add operation.
68+
let query = [kSecClass: kSecClassKey,
69+
kSecAttrApplicationLabel: label,
70+
kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,
71+
kSecUseDataProtectionKeychain: true,
72+
kSecValueRef: secKey] as [String: Any]
73+
74+
// Add the key to the keychain.
75+
let status = SecItemAdd(query as CFDictionary, nil)
76+
77+
switch status {
78+
case errSecSuccess:
79+
return true
80+
default:
81+
if let statusString = SecCopyErrorMessageString(status, nil)
82+
{
83+
print("Unable to store item: \(statusString)")
84+
}
85+
86+
return false
87+
}
88+
}
89+
90+
func retrievePrivateKey(query: CFDictionary) -> P256.KeyAgreement.PrivateKey?
91+
{
92+
// Find and cast the result as a SecKey instance.
93+
var item: CFTypeRef?
94+
var secKey: SecKey
95+
switch SecItemCopyMatching(query as CFDictionary, &item) {
96+
case errSecSuccess: secKey = item as! SecKey
97+
case errSecItemNotFound: return nil
98+
case let status:
99+
print("Keychain read failed: \(status)")
100+
return nil
101+
}
102+
103+
// Convert the SecKey into a CryptoKit key.
104+
var error: Unmanaged<CFError>?
105+
guard let data = SecKeyCopyExternalRepresentation(secKey, &error) as Data?
106+
else
107+
{
108+
print(error.debugDescription)
109+
return nil
110+
}
111+
112+
do {
113+
let key = try P256.KeyAgreement.PrivateKey(x963Representation: data)
114+
return key
115+
}
116+
catch let keyError
117+
{
118+
print("Error decoding key: \(keyError)")
119+
return nil
120+
}
121+
}
122+
123+
func generateKeySearchQuery(label: String, tag: String) -> CFDictionary
124+
{
125+
let query: [String: Any] = [kSecClass as String: kSecClassKey,
126+
kSecAttrApplicationLabel as String: label,
127+
kSecAttrApplicationTag as String: tag,
128+
kSecMatchLimit as String: kSecMatchLimitOne,
129+
kSecReturnRef as String: true,
130+
kSecReturnAttributes as String: false,
131+
kSecReturnData as String: false]
132+
133+
return query as CFDictionary
134+
}
135+
136+
// func generateKeyAttributesDictionary(tag: String) -> CFDictionary
137+
// {
138+
// //FIXME: Secure Enclave
139+
// // let access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleAlwaysThisDeviceOnly, .privateKeyUsage, nil)!
140+
//
141+
// let privateKeyAttributes: [String: Any] = [
142+
// kSecAttrIsPermanent as String: true,
143+
// kSecAttrApplicationTag as String: tag
144+
// //kSecAttrAccessControl as String: access
145+
// ]
146+
//
147+
// let publicKeyAttributes: [String: Any] = [
148+
// kSecAttrIsPermanent as String: true,
149+
// kSecAttrApplicationTag as String: tag
150+
// ]
151+
//
152+
// let attributes: [String: Any] = [
153+
// kSecClass as String: kSecClassKey,
154+
// kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
155+
// kSecAttrKeySizeInBits as String: 256,
156+
// //kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
157+
// kSecPrivateKeyAttrs as String: privateKeyAttributes,
158+
// kSecPublicKeyAttrs as String: publicKeyAttributes
159+
// ]
160+
//
161+
// return attributes as CFDictionary
162+
// }
163+
164+
public func deriveSymmetricKey(receiverPublicKey: P256.KeyAgreement.PublicKey, senderPrivateKey:P256.KeyAgreement.PrivateKey) -> SymmetricKey?
165+
{
166+
do
167+
{
168+
let sharedSecret = try senderPrivateKey.sharedSecretFromKeyAgreement(with: receiverPublicKey)
169+
let symmetricKey = sharedSecret.x963DerivedSymmetricKey(using: SHA256.self, sharedInfo: Data(), outputByteCount: 32)
170+
171+
return symmetricKey
172+
}
173+
catch let sharedSecretError
174+
{
175+
print("Unable to encrypt payload. Failed to generate a shared secret: \(sharedSecretError)")
176+
return nil
177+
}
178+
}
179+
180+
func deleteKeys(tag: String)
181+
{
182+
print("\nAttempted to delete key from secure enclave.")
183+
//Remove client keys from secure enclave
184+
let query: [String: Any] = [kSecClass as String: kSecClassKey,
185+
kSecAttrApplicationTag as String: tag]
186+
let deleteStatus = SecItemDelete(query as CFDictionary)
187+
188+
switch deleteStatus
189+
{
190+
case errSecItemNotFound:
191+
print("Could not find a client key to delete.\n")
192+
case noErr:
193+
print("Deleted client keys.\n")
194+
default:
195+
print("Unexpected status: \(deleteStatus.description)\n")
196+
}
197+
}
198+
}
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import XCTest
2+
@testable import Keychain
3+
4+
final class KeychainTests: XCTestCase {
5+
func testExample() {
6+
// This is an example of a functional test case.
7+
// Use XCTAssert and related functions to verify your tests produce the correct
8+
// results.
9+
}
10+
11+
static var allTests = [
12+
("testExample", testExample),
13+
]
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import XCTest
2+
3+
#if !canImport(ObjectiveC)
4+
public func allTests() -> [XCTestCaseEntry] {
5+
return [
6+
testCase(KeychainTests.allTests),
7+
]
8+
}
9+
#endif

Tests/LinuxMain.swift

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import XCTest
2+
3+
import KeychainTests
4+
5+
var tests = [XCTestCaseEntry]()
6+
tests += KeychainTests.allTests()
7+
XCTMain(tests)

0 commit comments

Comments
 (0)