Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41b6644b18 | |||
| a449ebf527 | |||
| ccda8fb901 | |||
| f94eff2115 | |||
| ee96b18cfd | |||
| 79cf22216f | |||
| 4bfbf86f73 | |||
| f0c4572ff4 | |||
| af441ab648 | |||
| 6074c2640a | |||
| 351b0a4c82 | |||
| 38b29df3ca | |||
| 9efa16d266 | |||
| 709d323463 | |||
| e06271dd90 | |||
| 1b40db9405 | |||
| afe4706789 | |||
| a89f2584ce | |||
| cb920c0318 | |||
| b11dea36b4 | |||
| 07a9234d3e | |||
| b193e300a5 | |||
| 7e53fc8322 | |||
| 9ce641f262 | |||
| 17d52574ab | |||
| 5602015a6d | |||
| 737c262e8b | |||
| 8dc3ef57c6 | |||
| 7a6395f189 | |||
| c48ce652bd | |||
| 716759b767 | |||
| 3da0e97cc6 | |||
| 03b5015f4f | |||
| 1b0c092708 | |||
| 25917eaf15 | |||
| 0dda3a7208 | |||
| 4dce464fb0 | |||
| 5e508a7551 | |||
| a2e58ce87f | |||
| 825d7d2485 | |||
| 0418e37185 | |||
| c218c1fbea | |||
| 68515bdc2c | |||
| f38046bb9e | |||
| e804dd0452 | |||
| 04c6084a8f | |||
| fbc549973b | |||
| 4ad7ca14e6 | |||
| 582f74e305 | |||
| 1758691bc2 | |||
| 2a21d9d0d1 | |||
| ca70968264 | |||
| 8017ad0d4e | |||
| e77cffad00 | |||
| 8d825ab368 | |||
| fb4030034d | |||
| 8f21125a77 | |||
| 3d6ce4d5f0 | |||
| 4caf3e99b4 | |||
| d04d2fe9b7 | |||
| 440de6f490 | |||
| 658233b596 | |||
| 4246e19952 | |||
| b988530bce | |||
| 73d14afcfa | |||
| c8a14a87b0 | |||
| 73074c7755 | |||
| 480c51f0bc | |||
| 780e9afe24 | |||
| a64c1d9dc7 |
+2
-2
@@ -2,7 +2,7 @@
|
||||
# * http://www.objc.io/issue-6/travis-ci.html
|
||||
# * https://github.com/supermarin/xcpretty#usage
|
||||
|
||||
language: objective-c
|
||||
language: swift
|
||||
# cache: cocoapods
|
||||
# podfile: Example/Podfile
|
||||
# before_install:
|
||||
@@ -11,5 +11,5 @@ language: objective-c
|
||||
install:
|
||||
- gem install xcpretty --no-rdoc --no-ri --no-document --quiet
|
||||
script:
|
||||
- set -o pipefail && xcodebuild test -workspace Example/Locksmith.xcworkspace -scheme Locksmith-Example -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO | xcpretty -c
|
||||
- set -o pipefail && xcodebuild test -workspace Example/Locksmith.xcworkspace -scheme Locksmith -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO | xcpretty -c
|
||||
- pod lib lint --quick
|
||||
|
||||
@@ -203,13 +203,9 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
CLASSPREFIX = Locksmith;
|
||||
LastSwiftUpdateCheck = 0700;
|
||||
LastUpgradeCheck = 0510;
|
||||
ORGANIZATIONNAME = matthewpalmer;
|
||||
TargetAttributes = {
|
||||
6003F5AD195388D20070C39A = {
|
||||
TestTargetID = 6003F589195388D20070C39A;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 6003F585195388D10070C39A /* Build configuration list for PBXProject "Locksmith" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
@@ -485,7 +481,6 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 5EED6F71CD7E7E38799A9088 /* Pods-Tests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Locksmith.app/Locksmith";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
@@ -503,7 +498,6 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Tests/Tests-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
TEST_HOST = "$(BUNDLE_LOADER)";
|
||||
WRAPPER_EXTENSION = xctest;
|
||||
};
|
||||
name = Debug;
|
||||
@@ -512,7 +506,6 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 71D78BD7B80CF35F7AB983DA /* Pods-Tests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Locksmith.app/Locksmith";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
@@ -525,7 +518,6 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Tests/Tests-Bridging-Header.h";
|
||||
TEST_HOST = "$(BUNDLE_LOADER)";
|
||||
WRAPPER_EXTENSION = xctest;
|
||||
};
|
||||
name = Release;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
source 'https://github.com/CocoaPods/Specs.git'
|
||||
|
||||
use_frameworks!
|
||||
|
||||
platform :ios, '8.0'
|
||||
|
||||
target 'Locksmith', :exclusive => true do
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
PODS:
|
||||
- Expecta (0.3.1)
|
||||
- Locksmith (0.1.0)
|
||||
- Locksmith (1.2.1)
|
||||
- Specta (0.2.1)
|
||||
|
||||
DEPENDENCIES:
|
||||
@@ -10,11 +10,11 @@ DEPENDENCIES:
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Locksmith:
|
||||
:path: ../
|
||||
:path: "../"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Expecta: 03aabd0a89d8dea843baecb19a7fd7466a69a31d
|
||||
Locksmith: b9371e929ae783d9ee80af42278258e9cedda7f6
|
||||
Specta: 9141310f46b1f68b676650ff2854e1ed0b74163a
|
||||
Expecta: a354d4633409dd9fe8c4f5ff5130426adbe31628
|
||||
Locksmith: 770f6c5c6e5c5c712d1f00e709c62b8a7240c716
|
||||
Specta: 15a276a6343867b426d5ed135d5aa4d04123a573
|
||||
|
||||
COCOAPODS: 0.36.0.beta.1
|
||||
COCOAPODS: 0.36.3
|
||||
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
#
|
||||
# Be sure to run `pod lib lint Locksmith.podspec' to ensure this is a
|
||||
# valid spec and remove all comments before submitting the spec.
|
||||
#
|
||||
# Any lines starting with a # are optional, but encouraged
|
||||
#
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
|
||||
#
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "Locksmith"
|
||||
s.version = "1.1.0"
|
||||
s.summary = "Locksmith is a sane way to work with the iOS Keychain in Swift."
|
||||
s.description = <<-DESC
|
||||
Locksmith is a sane way to work with the iOS Keychain in Swift.
|
||||
It provides a fast and intuitive way to work with the C Keychain API.
|
||||
Results are provided as tuples, and errors are informative and easily detected.
|
||||
DESC
|
||||
s.homepage = "https://github.com/matthewpalmer/Locksmith"
|
||||
# s.screenshots = "www.example.com/screenshots_1", "www.example.com/screenshots_2"
|
||||
s.license = 'MIT'
|
||||
s.author = { "matthewpalmer" => "matt@matthewpalmer.net" }
|
||||
s.source = { :git => "https://github.com/matthewpalmer/Locksmith.git", :tag => s.version.to_s }
|
||||
s.social_media_url = 'https://twitter.com/_matthewpalmer'
|
||||
|
||||
s.platform = :ios, '8.0'
|
||||
s.requires_arc = true
|
||||
|
||||
s.source_files = 'Pod/Classes'
|
||||
s.resource_bundles = {
|
||||
'Locksmith' => ['Pod/Assets/*.png']
|
||||
}
|
||||
|
||||
# s.public_header_files = 'Pod/Classes/**/*.h'
|
||||
# s.frameworks = 'UIKit', 'MapKit'
|
||||
# s.dependency 'AFNetworking', '~> 2.3'
|
||||
end
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Locksmith",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.1",
|
||||
"summary": "Locksmith is a sane way to work with the iOS Keychain in Swift.",
|
||||
"description": " Locksmith is a sane way to work with the iOS Keychain in Swift.\n It provides a fast and intuitive way to work with the C Keychain API.\n Results are provided as tuples, and errors are informative and easily detected.\n",
|
||||
"homepage": "https://github.com/matthewpalmer/Locksmith",
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"source": {
|
||||
"git": "https://github.com/matthewpalmer/Locksmith.git",
|
||||
"tag": "1.1.0"
|
||||
"tag": "1.2.1"
|
||||
},
|
||||
"social_media_url": "https://twitter.com/_matthewpalmer",
|
||||
"platforms": {
|
||||
|
||||
Generated
+6
-6
@@ -1,6 +1,6 @@
|
||||
PODS:
|
||||
- Expecta (0.3.1)
|
||||
- Locksmith (0.1.0)
|
||||
- Locksmith (1.2.1)
|
||||
- Specta (0.2.1)
|
||||
|
||||
DEPENDENCIES:
|
||||
@@ -10,11 +10,11 @@ DEPENDENCIES:
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Locksmith:
|
||||
:path: ../
|
||||
:path: "../"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Expecta: 03aabd0a89d8dea843baecb19a7fd7466a69a31d
|
||||
Locksmith: b9371e929ae783d9ee80af42278258e9cedda7f6
|
||||
Specta: 9141310f46b1f68b676650ff2854e1ed0b74163a
|
||||
Expecta: a354d4633409dd9fe8c4f5ff5130426adbe31628
|
||||
Locksmith: 770f6c5c6e5c5c712d1f00e709c62b8a7240c716
|
||||
Specta: 15a276a6343867b426d5ed135d5aa4d04123a573
|
||||
|
||||
COCOAPODS: 0.36.0.beta.1
|
||||
COCOAPODS: 0.36.3
|
||||
|
||||
+1779
-6151
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<string>1.2.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
Example/Pods/Target Support Files/Pods-Locksmith-Locksmith/Pods-Locksmith-Locksmith-Private.xcconfig
Generated
+4
-3
@@ -3,7 +3,8 @@ CONFIGURATION_BUILD_DIR = $PODS_FRAMEWORK_BUILD_PATH
|
||||
FRAMEWORK_SEARCH_PATHS = "$PODS_FRAMEWORK_BUILD_PATH"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Private/Locksmith" "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/Expecta" "${PODS_ROOT}/Headers/Public/Locksmith" "${PODS_ROOT}/Headers/Public/Specta"
|
||||
OTHER_LDFLAGS = -ObjC
|
||||
OTHER_SWIFT_FLAGS = "-D COCOAPODS"
|
||||
OTHER_LDFLAGS = ${PODS_LOCKSMITH_LOCKSMITH_OTHER_LDFLAGS} -ObjC
|
||||
OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS"
|
||||
PODS_FRAMEWORK_BUILD_PATH = $(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/Pods-Locksmith
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
SKIP_INSTALL = YES
|
||||
Generated
+1
@@ -0,0 +1 @@
|
||||
PODS_LOCKSMITH_LOCKSMITH_OTHER_LDFLAGS = -framework "Security" -framework "UIKit"
|
||||
+3
-3
@@ -8,7 +8,7 @@
|
||||
|
||||
// Locksmith
|
||||
#define COCOAPODS_POD_AVAILABLE_Locksmith
|
||||
#define COCOAPODS_VERSION_MAJOR_Locksmith 0
|
||||
#define COCOAPODS_VERSION_MINOR_Locksmith 1
|
||||
#define COCOAPODS_VERSION_PATCH_Locksmith 0
|
||||
#define COCOAPODS_VERSION_MAJOR_Locksmith 1
|
||||
#define COCOAPODS_VERSION_MINOR_Locksmith 2
|
||||
#define COCOAPODS_VERSION_PATCH_Locksmith 1
|
||||
|
||||
|
||||
+7
-4
@@ -11,7 +11,7 @@ install_framework()
|
||||
local source="${BUILT_PRODUCTS_DIR}/Pods-Locksmith/$1"
|
||||
local destination="${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
||||
|
||||
if [ -L ${source} ]; then
|
||||
if [ -L "${source}" ]; then
|
||||
echo "Symlinked..."
|
||||
source=$(readlink "${source}")
|
||||
fi
|
||||
@@ -28,10 +28,13 @@ install_framework()
|
||||
local basename
|
||||
basename=$(echo $1 | sed -E s/\\..+// && exit ${PIPESTATUS[0]})
|
||||
local swift_runtime_libs
|
||||
swift_runtime_libs=$(xcrun otool -LX "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/$1/${basename}" | grep @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]})
|
||||
swift_runtime_libs=$(xcrun otool -LX "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/$1/${basename}" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]})
|
||||
for lib in $swift_runtime_libs; do
|
||||
echo "rsync -av \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\""
|
||||
rsync -av "${SWIFT_STDLIB_PATH}/${lib}" "${destination}"
|
||||
echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\""
|
||||
rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}"
|
||||
if [ "${CODE_SIGNING_REQUIRED}" == "YES" ]; then
|
||||
code_sign "${destination}/${lib}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -6,6 +6,8 @@ mkdir -p "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt
|
||||
> "$RESOURCES_TO_COPY"
|
||||
|
||||
XCASSET_FILES=""
|
||||
|
||||
install_resource()
|
||||
{
|
||||
case $1 in
|
||||
@@ -36,6 +38,7 @@ install_resource()
|
||||
xcrun mapc "${PODS_ROOT}/$1" "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$1" .xcmappingmodel`.cdm"
|
||||
;;
|
||||
*.xcassets)
|
||||
XCASSET_FILES="$XCASSET_FILES '${PODS_ROOT}/$1'"
|
||||
;;
|
||||
/*)
|
||||
echo "$1"
|
||||
@@ -54,7 +57,7 @@ if [[ "${ACTION}" == "install" ]]; then
|
||||
fi
|
||||
rm -f "$RESOURCES_TO_COPY"
|
||||
|
||||
if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ `find . -name '*.xcassets' | wc -l` -ne 0 ]
|
||||
if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ]
|
||||
then
|
||||
case "${TARGETED_DEVICE_FAMILY}" in
|
||||
1,2)
|
||||
@@ -70,5 +73,6 @@ then
|
||||
TARGET_DEVICE_ARGS="--target-device mac"
|
||||
;;
|
||||
esac
|
||||
find "${PWD}" -name "*.xcassets" -print0 | xargs -0 actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${IPHONEOS_DEPLOYMENT_TARGET}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
while read line; do XCASSET_FILES="$XCASSET_FILES '$line'"; done <<<$(find "$PWD" -name "*.xcassets" | egrep -v "^$PODS_ROOT")
|
||||
echo $XCASSET_FILES | xargs actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${IPHONEOS_DEPLOYMENT_TARGET}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
fi
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ FRAMEWORK_SEARCH_PATHS = "$PODS_FRAMEWORK_BUILD_PATH"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
OTHER_CFLAGS = $(inherited) -iquote "$PODS_FRAMEWORK_BUILD_PATH/Locksmith.framework/Headers"
|
||||
OTHER_LDFLAGS = -ObjC -framework "Locksmith"
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -framework "Locksmith"
|
||||
OTHER_LIBTOOLFLAGS = $(OTHER_LDFLAGS)
|
||||
OTHER_SWIFT_FLAGS = "-D COCOAPODS"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS"
|
||||
PODS_FRAMEWORK_BUILD_PATH = $(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/Pods-Locksmith
|
||||
PODS_ROOT = ${SRCROOT}/Pods
|
||||
+2
-2
@@ -2,8 +2,8 @@ FRAMEWORK_SEARCH_PATHS = "$PODS_FRAMEWORK_BUILD_PATH"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
OTHER_CFLAGS = $(inherited) -iquote "$PODS_FRAMEWORK_BUILD_PATH/Locksmith.framework/Headers"
|
||||
OTHER_LDFLAGS = -ObjC -framework "Locksmith"
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -framework "Locksmith"
|
||||
OTHER_LIBTOOLFLAGS = $(OTHER_LDFLAGS)
|
||||
OTHER_SWIFT_FLAGS = "-D COCOAPODS"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS"
|
||||
PODS_FRAMEWORK_BUILD_PATH = $(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/Pods-Locksmith
|
||||
PODS_ROOT = ${SRCROOT}/Pods
|
||||
+2
-1
@@ -5,4 +5,5 @@ GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Private/Expecta" "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/Expecta" "${PODS_ROOT}/Headers/Public/Locksmith" "${PODS_ROOT}/Headers/Public/Specta"
|
||||
OTHER_LDFLAGS = ${PODS_TESTS_EXPECTA_OTHER_LDFLAGS} -ObjC
|
||||
PODS_FRAMEWORK_BUILD_PATH = $(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/Pods-Tests
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
SKIP_INSTALL = YES
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<string>1.2.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
Generated
+4
-3
@@ -3,7 +3,8 @@ CONFIGURATION_BUILD_DIR = $PODS_FRAMEWORK_BUILD_PATH
|
||||
FRAMEWORK_SEARCH_PATHS = "$PODS_FRAMEWORK_BUILD_PATH"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Private/Locksmith" "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/Expecta" "${PODS_ROOT}/Headers/Public/Locksmith" "${PODS_ROOT}/Headers/Public/Specta"
|
||||
OTHER_LDFLAGS = -ObjC
|
||||
OTHER_SWIFT_FLAGS = "-D COCOAPODS"
|
||||
OTHER_LDFLAGS = ${PODS_TESTS_LOCKSMITH_OTHER_LDFLAGS} -ObjC
|
||||
OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS"
|
||||
PODS_FRAMEWORK_BUILD_PATH = $(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/Pods-Tests
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
SKIP_INSTALL = YES
|
||||
+1
@@ -0,0 +1 @@
|
||||
PODS_TESTS_LOCKSMITH_OTHER_LDFLAGS = -framework "Security" -framework "UIKit"
|
||||
+2
-1
@@ -5,4 +5,5 @@ GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Private/Specta" "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/Expecta" "${PODS_ROOT}/Headers/Public/Locksmith" "${PODS_ROOT}/Headers/Public/Specta"
|
||||
OTHER_LDFLAGS = ${PODS_TESTS_SPECTA_OTHER_LDFLAGS} -ObjC
|
||||
PODS_FRAMEWORK_BUILD_PATH = $(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/Pods-Tests
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
SKIP_INSTALL = YES
|
||||
@@ -14,9 +14,9 @@
|
||||
|
||||
// Locksmith
|
||||
#define COCOAPODS_POD_AVAILABLE_Locksmith
|
||||
#define COCOAPODS_VERSION_MAJOR_Locksmith 0
|
||||
#define COCOAPODS_VERSION_MINOR_Locksmith 1
|
||||
#define COCOAPODS_VERSION_PATCH_Locksmith 0
|
||||
#define COCOAPODS_VERSION_MAJOR_Locksmith 1
|
||||
#define COCOAPODS_VERSION_MINOR_Locksmith 2
|
||||
#define COCOAPODS_VERSION_PATCH_Locksmith 1
|
||||
|
||||
// Specta
|
||||
#define COCOAPODS_POD_AVAILABLE_Specta
|
||||
|
||||
@@ -11,7 +11,7 @@ install_framework()
|
||||
local source="${BUILT_PRODUCTS_DIR}/Pods-Tests/$1"
|
||||
local destination="${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
||||
|
||||
if [ -L ${source} ]; then
|
||||
if [ -L "${source}" ]; then
|
||||
echo "Symlinked..."
|
||||
source=$(readlink "${source}")
|
||||
fi
|
||||
@@ -28,10 +28,13 @@ install_framework()
|
||||
local basename
|
||||
basename=$(echo $1 | sed -E s/\\..+// && exit ${PIPESTATUS[0]})
|
||||
local swift_runtime_libs
|
||||
swift_runtime_libs=$(xcrun otool -LX "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/$1/${basename}" | grep @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]})
|
||||
swift_runtime_libs=$(xcrun otool -LX "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/$1/${basename}" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]})
|
||||
for lib in $swift_runtime_libs; do
|
||||
echo "rsync -av \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\""
|
||||
rsync -av "${SWIFT_STDLIB_PATH}/${lib}" "${destination}"
|
||||
echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\""
|
||||
rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}"
|
||||
if [ "${CODE_SIGNING_REQUIRED}" == "YES" ]; then
|
||||
code_sign "${destination}/${lib}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ mkdir -p "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt
|
||||
> "$RESOURCES_TO_COPY"
|
||||
|
||||
XCASSET_FILES=""
|
||||
|
||||
install_resource()
|
||||
{
|
||||
case $1 in
|
||||
@@ -36,6 +38,7 @@ install_resource()
|
||||
xcrun mapc "${PODS_ROOT}/$1" "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$1" .xcmappingmodel`.cdm"
|
||||
;;
|
||||
*.xcassets)
|
||||
XCASSET_FILES="$XCASSET_FILES '${PODS_ROOT}/$1'"
|
||||
;;
|
||||
/*)
|
||||
echo "$1"
|
||||
@@ -54,7 +57,7 @@ if [[ "${ACTION}" == "install" ]]; then
|
||||
fi
|
||||
rm -f "$RESOURCES_TO_COPY"
|
||||
|
||||
if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ `find . -name '*.xcassets' | wc -l` -ne 0 ]
|
||||
if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ]
|
||||
then
|
||||
case "${TARGETED_DEVICE_FAMILY}" in
|
||||
1,2)
|
||||
@@ -70,5 +73,6 @@ then
|
||||
TARGET_DEVICE_ARGS="--target-device mac"
|
||||
;;
|
||||
esac
|
||||
find "${PWD}" -name "*.xcassets" -print0 | xargs -0 actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${IPHONEOS_DEPLOYMENT_TARGET}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
while read line; do XCASSET_FILES="$XCASSET_FILES '$line'"; done <<<$(find "$PWD" -name "*.xcassets" | egrep -v "^$PODS_ROOT")
|
||||
echo $XCASSET_FILES | xargs actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${IPHONEOS_DEPLOYMENT_TARGET}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
fi
|
||||
|
||||
@@ -2,8 +2,8 @@ FRAMEWORK_SEARCH_PATHS = "$PODS_FRAMEWORK_BUILD_PATH"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
OTHER_CFLAGS = $(inherited) -iquote "$PODS_FRAMEWORK_BUILD_PATH/Expecta.framework/Headers" -iquote "$PODS_FRAMEWORK_BUILD_PATH/Locksmith.framework/Headers" -iquote "$PODS_FRAMEWORK_BUILD_PATH/Specta.framework/Headers"
|
||||
OTHER_LDFLAGS = -ObjC -framework "Expecta" -framework "Locksmith" -framework "Specta"
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -framework "Expecta" -framework "Locksmith" -framework "Specta"
|
||||
OTHER_LIBTOOLFLAGS = $(OTHER_LDFLAGS)
|
||||
OTHER_SWIFT_FLAGS = "-D COCOAPODS"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS"
|
||||
PODS_FRAMEWORK_BUILD_PATH = $(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/Pods-Tests
|
||||
PODS_ROOT = ${SRCROOT}/Pods
|
||||
@@ -2,8 +2,8 @@ FRAMEWORK_SEARCH_PATHS = "$PODS_FRAMEWORK_BUILD_PATH"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
OTHER_CFLAGS = $(inherited) -iquote "$PODS_FRAMEWORK_BUILD_PATH/Expecta.framework/Headers" -iquote "$PODS_FRAMEWORK_BUILD_PATH/Locksmith.framework/Headers" -iquote "$PODS_FRAMEWORK_BUILD_PATH/Specta.framework/Headers"
|
||||
OTHER_LDFLAGS = -ObjC -framework "Expecta" -framework "Locksmith" -framework "Specta"
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -framework "Expecta" -framework "Locksmith" -framework "Specta"
|
||||
OTHER_LIBTOOLFLAGS = $(OTHER_LDFLAGS)
|
||||
OTHER_SWIFT_FLAGS = "-D COCOAPODS"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS"
|
||||
PODS_FRAMEWORK_BUILD_PATH = $(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/Pods-Tests
|
||||
PODS_ROOT = ${SRCROOT}/Pods
|
||||
@@ -2,106 +2,414 @@
|
||||
// LocksmithTests.swift
|
||||
// LocksmithTests
|
||||
//
|
||||
// Copyright (c) 2014 Mathew Palmer. All rights reserved.
|
||||
// Created by Matthew Palmer on 27/06/2015.
|
||||
// Copyright © 2015 Matthew Palmer. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import XCTest
|
||||
import Locksmith
|
||||
|
||||
class LocksmithTests: XCTestCase {
|
||||
let userAccount = "myUser"
|
||||
let service = "myService"
|
||||
|
||||
typealias TestingDictionaryType = [String: String]
|
||||
|
||||
func clear() {
|
||||
do {
|
||||
try Locksmith.deleteDataForUserAccount(userAccount, inService: service)
|
||||
try Locksmith.deleteDataForUserAccount(userAccount)
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
Locksmith.clearKeychain()
|
||||
clear()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
super.tearDown()
|
||||
clear()
|
||||
}
|
||||
|
||||
// public class func saveData(data: Dictionary<String, String>, inService service: String, forUserAccount userAccount: String) -> NSError?
|
||||
func testSaveData_Once() {
|
||||
var error = Locksmith.saveData(["key": "value"], inService: "myService", forUserAccount: "myUserAccount")
|
||||
XCTAssert(error == nil, "❌: saving data")
|
||||
func testStaticMethods() {
|
||||
let data = ["some": "data"]
|
||||
try! Locksmith.saveData(data, forUserAccount: userAccount, inService: service)
|
||||
|
||||
let loaded = Locksmith.loadDataForUserAccount(userAccount, inService: service)! as! TestingDictionaryType
|
||||
XCTAssertEqual(loaded, data)
|
||||
|
||||
try! Locksmith.deleteDataForUserAccount(userAccount, inService: service)
|
||||
|
||||
let otherData: TestingDictionaryType = ["something": "way different"]
|
||||
try! Locksmith.saveData(otherData, forUserAccount: userAccount, inService: service)
|
||||
|
||||
let loadedAgain = Locksmith.loadDataForUserAccount(userAccount, inService: service)! as! TestingDictionaryType
|
||||
XCTAssertEqual(loadedAgain, otherData)
|
||||
|
||||
let updatedData = ["this update": "brings the ruckus"]
|
||||
try! Locksmith.updateData(updatedData, forUserAccount: userAccount, inService: service)
|
||||
|
||||
let loaded3 = Locksmith.loadDataForUserAccount(userAccount, inService: service)! as! TestingDictionaryType
|
||||
|
||||
XCTAssertEqual(loaded3, updatedData)
|
||||
}
|
||||
|
||||
func testSaveData_Multiple() {
|
||||
var errors: [NSError?] = []
|
||||
for i in 0...10 {
|
||||
errors.append(Locksmith.saveData(["key": "value \(i)"], inService: "myService", forUserAccount: "myAccount\(i)"))
|
||||
func testStaticMethodsForDefaultService() {
|
||||
let data = ["some": "data"]
|
||||
try! Locksmith.saveData(data, forUserAccount: userAccount)
|
||||
|
||||
let loaded = Locksmith.loadDataForUserAccount(userAccount)! as! TestingDictionaryType
|
||||
XCTAssertEqual(loaded, data)
|
||||
|
||||
try! Locksmith.deleteDataForUserAccount(userAccount)
|
||||
|
||||
let otherData: TestingDictionaryType = ["something": "way different"]
|
||||
try! Locksmith.saveData(otherData, forUserAccount: userAccount)
|
||||
|
||||
let loadedAgain = Locksmith.loadDataForUserAccount(userAccount)! as! TestingDictionaryType
|
||||
XCTAssertEqual(loadedAgain, otherData)
|
||||
|
||||
let updatedData = ["this update": "brings the ruckus"]
|
||||
try! Locksmith.updateData(updatedData, forUserAccount: userAccount)
|
||||
|
||||
let loaded3 = Locksmith.loadDataForUserAccount(userAccount)! as! TestingDictionaryType
|
||||
|
||||
XCTAssertEqual(loaded3, updatedData)
|
||||
}
|
||||
|
||||
func createGenericPasswordWithData(data: [String: AnyObject]) {
|
||||
struct CreateGenericPassword: CreateableSecureStorable, GenericPasswordSecureStorable {
|
||||
let data: [String: AnyObject]
|
||||
let account: String
|
||||
let service: String
|
||||
}
|
||||
XCTAssert(errors.filter({ $0 != nil }).isEmpty, "❌: saving multiple items")
|
||||
|
||||
let create = CreateGenericPassword(data: data, account: userAccount, service: service)
|
||||
try! create.createInSecureStore() // make sure it doesn't throw
|
||||
}
|
||||
|
||||
func testSaveData_Duplicate() {
|
||||
// Should be successful
|
||||
let error1 = Locksmith.saveData(["key": "value"], inService: "myService", forUserAccount: "user")
|
||||
|
||||
// Should fail
|
||||
let error2 = Locksmith.saveData(["key": "value"], inService: "myService", forUserAccount: "user")
|
||||
|
||||
XCTAssert(error1 == nil && error2 != nil, "❌: saving duplicate data")
|
||||
func testCreateForGenericPassword() {
|
||||
let data = ["some": "data"]
|
||||
createGenericPasswordWithData(data)
|
||||
}
|
||||
|
||||
// Setup the keychain for requests that use pre-existing values on the keychain (update, read, delete)
|
||||
func setupLoads() {
|
||||
Locksmith.saveData(["key": "value"], inService: "myService", forUserAccount: "user1")
|
||||
Locksmith.saveData(["anotherkey": "anothervalue"], inService: "myService", forUserAccount: "user2")
|
||||
Locksmith.saveData(["word": "definition"], inService: "myService", forUserAccount: "user3")
|
||||
func testLoadForGenericPassword() {
|
||||
let data = ["one": "two"]
|
||||
createGenericPasswordWithData(data)
|
||||
|
||||
struct ReadGenericPassword: ReadableSecureStorable, GenericPasswordSecureStorable {
|
||||
let account: String
|
||||
let service: String
|
||||
}
|
||||
|
||||
let read = ReadGenericPassword(account: userAccount, service: service)
|
||||
let actual = read.readFromSecureStore()!.data as! TestingDictionaryType
|
||||
XCTAssertEqual(actual, data)
|
||||
}
|
||||
|
||||
// public class func loadDataInService(service: String, forUserAccount userAccount: String) -> (NSDictionary?, NSError?)
|
||||
func testLoadData_Once() {
|
||||
setupLoads()
|
||||
func testDeleteForGenericPassword() {
|
||||
let initialData = ["one": "two"]
|
||||
|
||||
let (dictionary, error) = Locksmith.loadDataInService("myService", forUserAccount: "user1")
|
||||
XCTAssert(dictionary!.valueForKey("key")! as NSString == "value" && error == nil, "❌: loading one item")
|
||||
createGenericPasswordWithData(initialData)
|
||||
|
||||
struct DeleteGenericPassword: DeleteableSecureStorable, GenericPasswordSecureStorable {
|
||||
let account: String
|
||||
let service: String
|
||||
}
|
||||
|
||||
let delete = DeleteGenericPassword(account: userAccount, service: service)
|
||||
try! delete.deleteFromSecureStore()
|
||||
|
||||
let d = Locksmith.loadDataForUserAccount(userAccount, inService: service)
|
||||
XCTAssert(d == nil)
|
||||
}
|
||||
|
||||
func testLoadData_Multiple() {
|
||||
setupLoads()
|
||||
func testForConformanceToAll3Protocols() {
|
||||
struct Omnivore: ReadableSecureStorable, CreateableSecureStorable, DeleteableSecureStorable, GenericPasswordSecureStorable {
|
||||
let account: String
|
||||
let service: String
|
||||
let data: [String: AnyObject]
|
||||
}
|
||||
|
||||
let (dictionary, error) = Locksmith.loadDataInService("myService", forUserAccount: "user1")
|
||||
let (dictionary2, error2) = Locksmith.loadDataInService("myService", forUserAccount: "user2")
|
||||
let (dictionary3, error3) = Locksmith.loadDataInService("myService", forUserAccount: "user3")
|
||||
let data: [String: String] = ["something": "else"]
|
||||
let omni = Omnivore(account: userAccount, service: service, data: data)
|
||||
|
||||
XCTAssert(dictionary!.valueForKey("key")! as NSString == "value" && error == nil, "❌: loading multiple items")
|
||||
XCTAssert(dictionary2!.valueForKey("anotherkey")! as NSString == "anothervalue" && error == nil, "❌: loading multiple items")
|
||||
XCTAssert(dictionary3!.valueForKey("word")! as NSString == "definition" && error == nil, "❌: loading multiple items")
|
||||
try! omni.createInSecureStore()
|
||||
|
||||
let result = omni.readFromSecureStore()
|
||||
let resultData = result?.data as! [String: String]
|
||||
|
||||
XCTAssertEqual(result?.account, userAccount)
|
||||
XCTAssertEqual(result?.service, service)
|
||||
XCTAssertEqual(resultData, data)
|
||||
|
||||
try! omni.deleteFromSecureStore()
|
||||
|
||||
let noResult = omni.readFromSecureStore()
|
||||
XCTAssertNil(noResult?.service)
|
||||
|
||||
try! omni.createInSecureStore()
|
||||
XCTAssertEqual(result?.account, userAccount)
|
||||
XCTAssertEqual(result?.service, service)
|
||||
XCTAssertEqual(resultData, data)
|
||||
}
|
||||
|
||||
// public class func updateData(data: Dictionary<String, String>, inService service: String, forUserAccount userAccount: String) -> NSError?
|
||||
func testUpdateData() {
|
||||
setupLoads()
|
||||
func testDeleteForInternetPassword() {
|
||||
struct Create : CreateableSecureStorable, InternetPasswordSecureStorable {
|
||||
let account: String
|
||||
let server: String
|
||||
let data: [String: AnyObject]
|
||||
let port: Int
|
||||
let internetProtocol: LocksmithInternetProtocol
|
||||
let authenticationType: LocksmithInternetAuthenticationType
|
||||
}
|
||||
|
||||
let error = Locksmith.updateData(["key": "newvalue"], inService: "myService", forUserAccount: "user1")
|
||||
let (dictionary, err) = Locksmith.loadDataInService("myService", forUserAccount: "user1")
|
||||
XCTAssert(dictionary!.valueForKey("key")! as NSString == "newvalue" && error == nil, "❌: updating item")
|
||||
let server = "server"
|
||||
let initialData = ["one": "two"]
|
||||
let port = 8080
|
||||
let internetProtocol = LocksmithInternetProtocol.HTTPS
|
||||
let authenticationType = LocksmithInternetAuthenticationType.DPA
|
||||
|
||||
// Updating an item that doesn't exist should create that item (i.e. performs a regular create request)
|
||||
let error2 = Locksmith.updateData(["key": "anothervalue"], inService: "myService", forUserAccount: "user1")
|
||||
XCTAssert(error2 == nil, "❌: updating item that doesn't exist")
|
||||
struct Delete: DeleteableSecureStorable, InternetPasswordSecureStorable {
|
||||
let account: String
|
||||
let server: String
|
||||
let port: Int
|
||||
let internetProtocol: LocksmithInternetProtocol
|
||||
let authenticationType: LocksmithInternetAuthenticationType
|
||||
}
|
||||
|
||||
struct Read: ReadableSecureStorable, InternetPasswordSecureStorable {
|
||||
let account: String
|
||||
let server: String
|
||||
let port: Int
|
||||
let internetProtocol: LocksmithInternetProtocol
|
||||
let authenticationType: LocksmithInternetAuthenticationType
|
||||
}
|
||||
|
||||
let c = Create(account: userAccount, server: server, data: initialData, port: port, internetProtocol: internetProtocol, authenticationType: authenticationType)
|
||||
try! c.createInSecureStore()
|
||||
let r1 = Read(account: userAccount, server: server, port: port, internetProtocol: internetProtocol, authenticationType: authenticationType)
|
||||
let result1 = r1.readFromSecureStore()
|
||||
XCTAssertEqual(result1?.server, server)
|
||||
|
||||
let d = Delete(account: userAccount, server: server, port: port, internetProtocol: internetProtocol, authenticationType: authenticationType)
|
||||
try! d.deleteFromSecureStore()
|
||||
|
||||
let result2 = r1.readFromSecureStore()
|
||||
XCTAssertEqual(result2?.server, nil)
|
||||
}
|
||||
|
||||
// public class func deleteDataInService(service: String, forUserAccount userAccount: String) -> NSError?
|
||||
func testDeleteData() {
|
||||
setupLoads()
|
||||
func testGenericPasswordMetaAttributesAreCreatedAndReturned() {
|
||||
struct Create: CreateableSecureStorable, GenericPasswordSecureStorable {
|
||||
let account: String
|
||||
let service: String
|
||||
let comment: String?
|
||||
let description: String?
|
||||
let creator: UInt?
|
||||
let data: [String: AnyObject]
|
||||
}
|
||||
|
||||
let error = Locksmith.deleteDataInService("myService", forUserAccount: "user1")
|
||||
XCTAssert(error == nil, "❌: deleting existing item")
|
||||
let initialData = ["one": "two"]
|
||||
let creator: UInt = 5
|
||||
let comment = "this is a comment"
|
||||
let description = "this is the description"
|
||||
let c = Create(account: userAccount, service: service, comment: comment, description: description, creator: creator, data: initialData)
|
||||
try! c.createInSecureStore()
|
||||
|
||||
let error2 = Locksmith.deleteDataInService("myService", forUserAccount: "user1")
|
||||
XCTAssert(error2 != nil, "❌: deleting non existent item")
|
||||
struct Read: ReadableSecureStorable, GenericPasswordSecureStorable {
|
||||
let account: String
|
||||
let service: String
|
||||
}
|
||||
|
||||
let r = Read(account: userAccount, service: service)
|
||||
let d = r.readFromSecureStore()
|
||||
|
||||
XCTAssertEqual(d?.account, userAccount)
|
||||
XCTAssertEqual(d?.service, service)
|
||||
XCTAssertEqual(d!.data as! [String: String], initialData)
|
||||
XCTAssertEqual(d?.creator, creator)
|
||||
XCTAssertEqual(d?.comment, comment)
|
||||
XCTAssertEqual(d?.description, description)
|
||||
|
||||
XCTAssertNil(d?.generic)
|
||||
XCTAssertNil(d?.isInvisible)
|
||||
}
|
||||
|
||||
func testPerformanceExample() {
|
||||
// This is an example of a performance test case.
|
||||
self.measureBlock() {
|
||||
// Put the code you want to measure the time of here.
|
||||
func testInternetPasswordMetaAttributesAreCreatedAndReturned() {
|
||||
struct CreateInternetPassword: CreateableSecureStorable, InternetPasswordSecureStorable {
|
||||
let account: String
|
||||
let data: [String: AnyObject]
|
||||
let server: String
|
||||
let port: Int
|
||||
let internetProtocol: LocksmithInternetProtocol
|
||||
let authenticationType: LocksmithInternetAuthenticationType
|
||||
let path: String?
|
||||
let securityDomain: String?
|
||||
}
|
||||
|
||||
let userAccount = "user \(NSDate())"
|
||||
let initialData = ["internet": "data"]
|
||||
let server = "net.matthewpalmer"
|
||||
let port = 8080
|
||||
let internetProtocol = LocksmithInternetProtocol.FTP
|
||||
let authenticationType = LocksmithInternetAuthenticationType.HTTPBasic
|
||||
let path = "somePath"
|
||||
let securityDomain = "someDomain"
|
||||
|
||||
struct ReadInternetPassword: ReadableSecureStorable, InternetPasswordSecureStorable {
|
||||
let account: String
|
||||
let server: String
|
||||
let port: Int
|
||||
let internetProtocol: LocksmithInternetProtocol
|
||||
let authenticationType: LocksmithInternetAuthenticationType
|
||||
}
|
||||
|
||||
let c = CreateInternetPassword(account: userAccount, data: initialData, server: server, port: port, internetProtocol: internetProtocol, authenticationType: authenticationType, path: path, securityDomain: securityDomain)
|
||||
try! c.createInSecureStore()
|
||||
|
||||
let r = ReadInternetPassword(account: userAccount, server: server, port: port, internetProtocol: internetProtocol, authenticationType: authenticationType)
|
||||
let result = r.readFromSecureStore()
|
||||
|
||||
XCTAssertEqual(result?.account, userAccount)
|
||||
XCTAssertEqual(result!.data as! [String: String], initialData)
|
||||
XCTAssertEqual(result?.server, server)
|
||||
XCTAssertEqual(result?.port, port)
|
||||
XCTAssertEqual(result?.internetProtocol, internetProtocol)
|
||||
XCTAssertEqual(result?.authenticationType, authenticationType)
|
||||
XCTAssertEqual(result?.securityDomain, securityDomain)
|
||||
XCTAssertEqual(result?.path, path)
|
||||
}
|
||||
|
||||
func assertStringPairsMatchInDictionary(dictionary: NSDictionary, pairs: [(key: CFString, expectedOutput: String)]) {
|
||||
for pair in pairs {
|
||||
let a = dictionary[String(pair.0)] as! CFStringRef
|
||||
XCTAssertEqual(a as String, pair.1)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
func testInternetPasswordAttributesAreAppliedForConformingTypes() {
|
||||
struct CreateInternetPassword: CreateableSecureStorable, InternetPasswordSecureStorable, DeleteableSecureStorable {
|
||||
let account: String
|
||||
let service: String
|
||||
let data: [String: AnyObject]
|
||||
let server: String
|
||||
let port: Int
|
||||
let internetProtocol: LocksmithInternetProtocol
|
||||
let authenticationType: LocksmithInternetAuthenticationType
|
||||
let path: String?
|
||||
let securityDomain: String?
|
||||
let performCreateRequestClosure: PerformRequestClosureType
|
||||
}
|
||||
|
||||
let account = "myUser"
|
||||
let port = 8080
|
||||
let internetProtocol = LocksmithInternetProtocol.HTTP
|
||||
let authenticationType = LocksmithInternetAuthenticationType.HTTPBasic
|
||||
let path = "some_path"
|
||||
let securityDomain = "secdomain"
|
||||
let data = ["some": "data"]
|
||||
let server = "server"
|
||||
|
||||
let expect = self.expectationWithDescription("Must enter the closure")
|
||||
|
||||
let performRequestClosure: PerformRequestClosureType = { (requestReference, result) in
|
||||
let dict = requestReference as NSDictionary
|
||||
|
||||
self.assertStringPairsMatchInDictionary(dict, pairs: [
|
||||
(kSecAttrAccount, account),
|
||||
(kSecAttrProtocol, internetProtocol.rawValue),
|
||||
(kSecAttrAuthenticationType, authenticationType.rawValue),
|
||||
(kSecAttrPath, path),
|
||||
(kSecAttrSecurityDomain, securityDomain),
|
||||
(kSecAttrServer, server),
|
||||
(kSecClass, String(kSecClassInternetPassword))
|
||||
])
|
||||
|
||||
let p = dict[String(kSecAttrPort)] as! CFNumberRef
|
||||
XCTAssertEqual(p as Int, port)
|
||||
|
||||
expect.fulfill()
|
||||
|
||||
return errSecSuccess
|
||||
}
|
||||
|
||||
let create = CreateInternetPassword(account: account, service: service, data: data, server: server, port: port, internetProtocol: internetProtocol, authenticationType: authenticationType, path: path, securityDomain: securityDomain, performCreateRequestClosure: performRequestClosure)
|
||||
do { try create.deleteFromSecureStore() } catch {}
|
||||
try! create.createInSecureStore()
|
||||
|
||||
self.waitForExpectationsWithTimeout(0.1, handler: nil)
|
||||
}
|
||||
|
||||
func testGenericPasswordOptionalAttributesAreAppliedForConformingTypes() {
|
||||
struct CreateGenericPassword: CreateableSecureStorable, GenericPasswordSecureStorable {
|
||||
let data: [String: AnyObject]
|
||||
let account: String
|
||||
let service: String
|
||||
let accessGroup: String?
|
||||
let description: String?
|
||||
let creator: UInt?
|
||||
var performCreateRequestClosure: PerformRequestClosureType
|
||||
let accessible: LocksmithAccessibleOption?
|
||||
let comment: String?
|
||||
let type: UInt?
|
||||
let isInvisible: Bool?
|
||||
let isNegative: Bool?
|
||||
let generic: NSData?
|
||||
}
|
||||
|
||||
let data: [String: AnyObject] = ["some": "data"]
|
||||
let account: String = "myUser"
|
||||
let service: String = "myService"
|
||||
let accessGroup: String = "myAccessGroup"
|
||||
let description: String = "myDescription"
|
||||
let creator: UInt = 5
|
||||
let accessible: LocksmithAccessibleOption = LocksmithAccessibleOption.Always
|
||||
let comment: String = "myComment"
|
||||
let type: UInt = 10
|
||||
let isInvisible: Bool = false
|
||||
let isNegative: Bool = false
|
||||
let generic: NSData = NSData()
|
||||
|
||||
let expect = self.expectationWithDescription("Must enter the closure")
|
||||
|
||||
let performRequestClosure: PerformRequestClosureType = { (requestReference, result) in
|
||||
let dict = requestReference as NSDictionary
|
||||
|
||||
self.assertStringPairsMatchInDictionary(dict, pairs: [
|
||||
(kSecAttrAccount, account),
|
||||
(kSecAttrService, service),
|
||||
(kSecAttrAccessGroup, accessGroup),
|
||||
(kSecAttrDescription, description),
|
||||
(kSecAttrComment, comment),
|
||||
(kSecAttrAccessible, accessible.rawValue),
|
||||
(kSecClass, String(kSecClassGenericPassword))
|
||||
])
|
||||
|
||||
let cr = dict[String(kSecAttrCreator)] as! CFNumberRef
|
||||
XCTAssertEqual(cr as UInt, creator)
|
||||
|
||||
let ty = dict[String(kSecAttrType)] as! CFNumberRef
|
||||
XCTAssertEqual(ty as UInt, type)
|
||||
|
||||
let inv = dict[String(kSecAttrIsInvisible)] as! CFBooleanRef
|
||||
XCTAssertEqual(inv as Bool, isInvisible)
|
||||
|
||||
let neg = dict[String(kSecAttrIsNegative)] as! CFBooleanRef
|
||||
XCTAssertEqual(neg as Bool, isNegative)
|
||||
|
||||
let gen = dict[String(kSecAttrGeneric)] as! CFDataRef
|
||||
XCTAssertEqual(gen, generic)
|
||||
|
||||
expect.fulfill()
|
||||
|
||||
return errSecSuccess
|
||||
}
|
||||
|
||||
let create: CreateGenericPassword = CreateGenericPassword(data: data, account: account, service: service, accessGroup: accessGroup, description: description, creator: creator, performCreateRequestClosure: performRequestClosure, accessible: accessible, comment: comment, type: type, isInvisible: isInvisible, isNegative: isNegative, generic: generic)
|
||||
|
||||
try! create.createInSecureStore()
|
||||
|
||||
self.waitForExpectationsWithTimeout(0.1, handler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-18
@@ -1,29 +1,17 @@
|
||||
#
|
||||
# Be sure to run `pod lib lint Locksmith.podspec' to ensure this is a
|
||||
# valid spec and remove all comments before submitting the spec.
|
||||
#
|
||||
# Any lines starting with a # are optional, but encouraged
|
||||
#
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
|
||||
#
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "Locksmith"
|
||||
s.version = "1.1.0"
|
||||
s.summary = "Locksmith is a sane way to work with the iOS Keychain in Swift."
|
||||
s.version = "2.0.0"
|
||||
s.summary = "Locksmith is a protocol-oriented way to work with the iOS Keychain in Swift."
|
||||
s.description = <<-DESC
|
||||
Locksmith is a sane way to work with the iOS Keychain in Swift.
|
||||
It provides a fast and intuitive way to work with the C Keychain API.
|
||||
Results are provided as tuples, and errors are informative and easily detected.
|
||||
Locksmith is a protocol-oriented way to work with the iOS Keychain in Swift. It provides extensive support for a lot of different keychain requests, and extensively uses Swift-native concepts.
|
||||
DESC
|
||||
s.homepage = "https://github.com/matthewpalmer/Locksmith"
|
||||
# s.screenshots = "www.example.com/screenshots_1", "www.example.com/screenshots_2"
|
||||
s.license = 'MIT'
|
||||
s.author = { "matthewpalmer" => "matt@matthewpalmer.net" }
|
||||
s.source = { :git => "https://github.com/matthewpalmer/Locksmith.git", :tag => s.version.to_s }
|
||||
s.social_media_url = 'https://twitter.com/_matthewpalmer'
|
||||
|
||||
s.platform = :ios, '8.0'
|
||||
s.platform = :ios, '9.0'
|
||||
s.requires_arc = true
|
||||
|
||||
s.source_files = 'Pod/Classes/*.{m,h,swift}'
|
||||
@@ -31,7 +19,5 @@ Pod::Spec.new do |s|
|
||||
'Locksmith' => ['Pod/Assets/*.png']
|
||||
}
|
||||
|
||||
# s.public_header_files = 'Pod/Classes/**/*.h'
|
||||
s.frameworks = 'UIKit', 'Security'
|
||||
# s.dependency 'AFNetworking', '~> 2.3'
|
||||
end
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
BFFB19D61A4870A300CCFFC3 /* Locksmith.h in Headers */ = {isa = PBXBuildFile; fileRef = BFFB19D51A4870A300CCFFC3 /* Locksmith.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
9D60D8141B10927B00BE14A9 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9D60D8131B10927B00BE14A9 /* Info.plist */; };
|
||||
9DF0A2C01B0E394F0049F83A /* Locksmith.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DF0A2BE1B0E394F0049F83A /* Locksmith.swift */; };
|
||||
9DF0A2C11B0E394F0049F83A /* LocksmithRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DF0A2BF1B0E394F0049F83A /* LocksmithRequest.swift */; };
|
||||
9DF0A2C81B0E3D370049F83A /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9DF0A2C71B0E3D370049F83A /* Info.plist */; };
|
||||
BFFB19DC1A4870A300CCFFC3 /* Locksmith.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFFB19D01A4870A300CCFFC3 /* Locksmith.framework */; };
|
||||
BFFB19E31A4870A300CCFFC3 /* LocksmithTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFB19E21A4870A300CCFFC3 /* LocksmithTests.swift */; };
|
||||
BFFB19EE1A4870E400CCFFC3 /* LocksmithRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFB19EC1A4870E400CCFFC3 /* LocksmithRequest.swift */; };
|
||||
BFFB19EF1A4870E400CCFFC3 /* Locksmith.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFB19ED1A4870E400CCFFC3 /* Locksmith.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -25,14 +26,13 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
9D60D8131B10927B00BE14A9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Pod/Info.plist; sourceTree = SOURCE_ROOT; };
|
||||
9DF0A2BE1B0E394F0049F83A /* Locksmith.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Locksmith.swift; path = Pod/Classes/Locksmith.swift; sourceTree = SOURCE_ROOT; };
|
||||
9DF0A2BF1B0E394F0049F83A /* LocksmithRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LocksmithRequest.swift; path = Pod/Classes/LocksmithRequest.swift; sourceTree = SOURCE_ROOT; };
|
||||
9DF0A2C71B0E3D370049F83A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
BFFB19D01A4870A300CCFFC3 /* Locksmith.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Locksmith.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFFB19D41A4870A300CCFFC3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
BFFB19D51A4870A300CCFFC3 /* Locksmith.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Locksmith.h; sourceTree = "<group>"; };
|
||||
BFFB19DB1A4870A300CCFFC3 /* LocksmithTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LocksmithTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFFB19E11A4870A300CCFFC3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
BFFB19E21A4870A300CCFFC3 /* LocksmithTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocksmithTests.swift; sourceTree = "<group>"; };
|
||||
BFFB19EC1A4870E400CCFFC3 /* LocksmithRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocksmithRequest.swift; sourceTree = "<group>"; };
|
||||
BFFB19ED1A4870E400CCFFC3 /* Locksmith.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Locksmith.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -75,9 +75,8 @@
|
||||
BFFB19D21A4870A300CCFFC3 /* Locksmith */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFFB19EC1A4870E400CCFFC3 /* LocksmithRequest.swift */,
|
||||
BFFB19ED1A4870E400CCFFC3 /* Locksmith.swift */,
|
||||
BFFB19D51A4870A300CCFFC3 /* Locksmith.h */,
|
||||
9DF0A2BE1B0E394F0049F83A /* Locksmith.swift */,
|
||||
9DF0A2BF1B0E394F0049F83A /* LocksmithRequest.swift */,
|
||||
BFFB19D31A4870A300CCFFC3 /* Supporting Files */,
|
||||
);
|
||||
path = Locksmith;
|
||||
@@ -86,7 +85,7 @@
|
||||
BFFB19D31A4870A300CCFFC3 /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFFB19D41A4870A300CCFFC3 /* Info.plist */,
|
||||
9D60D8131B10927B00BE14A9 /* Info.plist */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
@@ -103,7 +102,7 @@
|
||||
BFFB19E01A4870A300CCFFC3 /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFFB19E11A4870A300CCFFC3 /* Info.plist */,
|
||||
9DF0A2C71B0E3D370049F83A /* Info.plist */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
@@ -115,7 +114,6 @@
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BFFB19D61A4870A300CCFFC3 /* Locksmith.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -164,6 +162,7 @@
|
||||
BFFB19C71A4870A300CCFFC3 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0700;
|
||||
LastUpgradeCheck = 0610;
|
||||
ORGANIZATIONNAME = "Mathew Palmer";
|
||||
TargetAttributes = {
|
||||
@@ -198,6 +197,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9D60D8141B10927B00BE14A9 /* Info.plist in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -205,6 +205,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9DF0A2C81B0E3D370049F83A /* Info.plist in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -215,8 +216,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BFFB19EF1A4870E400CCFFC3 /* Locksmith.swift in Sources */,
|
||||
BFFB19EE1A4870E400CCFFC3 /* LocksmithRequest.swift in Sources */,
|
||||
9DF0A2C11B0E394F0049F83A /* LocksmithRequest.swift in Sources */,
|
||||
9DF0A2C01B0E394F0049F83A /* Locksmith.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -274,7 +275,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -314,7 +315,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -327,11 +328,12 @@
|
||||
BFFB19E71A4870A300CCFFC3 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Locksmith/Info.plist;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/Pod/Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -343,11 +345,12 @@
|
||||
BFFB19E81A4870A300CCFFC3 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Locksmith/Info.plist;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/Pod/Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -367,7 +370,7 @@
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = LocksmithTests/Info.plist;
|
||||
INFOPLIST_FILE = LockSmithTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
@@ -380,7 +383,7 @@
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = LocksmithTests/Info.plist;
|
||||
INFOPLIST_FILE = LockSmithTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0630"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFFB19CF1A4870A300CCFFC3"
|
||||
BuildableName = "Locksmith.framework"
|
||||
BlueprintName = "Locksmith"
|
||||
ReferencedContainer = "container:Locksmith.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFFB19DA1A4870A300CCFFC3"
|
||||
BuildableName = "LocksmithTests.xctest"
|
||||
BlueprintName = "LocksmithTests"
|
||||
ReferencedContainer = "container:Locksmith.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Debug">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFFB19DA1A4870A300CCFFC3"
|
||||
BuildableName = "LocksmithTests.xctest"
|
||||
BlueprintName = "LocksmithTests"
|
||||
ReferencedContainer = "container:Locksmith.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFFB19CF1A4870A300CCFFC3"
|
||||
BuildableName = "Locksmith.framework"
|
||||
BlueprintName = "Locksmith"
|
||||
ReferencedContainer = "container:Locksmith.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Debug"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFFB19CF1A4870A300CCFFC3"
|
||||
BuildableName = "Locksmith.framework"
|
||||
BlueprintName = "Locksmith"
|
||||
ReferencedContainer = "container:Locksmith.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Release"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFFB19CF1A4870A300CCFFC3"
|
||||
BuildableName = "Locksmith.framework"
|
||||
BlueprintName = "Locksmith"
|
||||
ReferencedContainer = "container:Locksmith.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// Locksmith.h
|
||||
// Locksmith
|
||||
//
|
||||
// Created by Michael Hahn on 12/22/14.
|
||||
// Copyright (c) 2014 Mathew Palmer. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
//! Project version number for Locksmith.
|
||||
FOUNDATION_EXPORT double LocksmithVersionNumber;
|
||||
|
||||
//! Project version string for Locksmith.
|
||||
FOUNDATION_EXPORT const unsigned char LocksmithVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <Locksmith/PublicHeader.h>
|
||||
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
//
|
||||
// Locksmith.swift
|
||||
// Locksmith-Demo
|
||||
//
|
||||
// Created by Matthew Palmer on 26/10/2014.
|
||||
// Copyright (c) 2014 Colour Coding. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public let LocksmithErrorDomain = "com.locksmith.error"
|
||||
|
||||
public class Locksmith: NSObject {
|
||||
// MARK: Perform request
|
||||
class func performRequest(request: LocksmithRequest) -> (NSDictionary?, NSError?) {
|
||||
let type = request.type
|
||||
//var result: Unmanaged<AnyObject>? = nil
|
||||
var result: AnyObject?
|
||||
var status: OSStatus?
|
||||
|
||||
var parsedRequest: NSMutableDictionary = parseRequest(request)
|
||||
|
||||
var requestReference = parsedRequest as CFDictionaryRef
|
||||
|
||||
switch type {
|
||||
case .Create:
|
||||
status = withUnsafeMutablePointer(&result) { SecItemAdd(requestReference, UnsafeMutablePointer($0)) }
|
||||
case .Read:
|
||||
status = withUnsafeMutablePointer(&result) { SecItemCopyMatching(requestReference, UnsafeMutablePointer($0)) }
|
||||
case .Delete:
|
||||
status = SecItemDelete(requestReference)
|
||||
case .Update:
|
||||
status = Locksmith.performUpdate(requestReference, result: &result)
|
||||
default:
|
||||
status = nil
|
||||
}
|
||||
|
||||
if let status = status {
|
||||
var statusCode = Int(status)
|
||||
let error = Locksmith.keychainError(forCode: statusCode)
|
||||
var resultsDictionary: NSDictionary?
|
||||
|
||||
if result != nil {
|
||||
if type == .Read && status == errSecSuccess {
|
||||
|
||||
if let data = result as? NSData {
|
||||
// Convert the retrieved data to a dictionary
|
||||
resultsDictionary = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? NSDictionary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (resultsDictionary, error)
|
||||
} else {
|
||||
let code = LocksmithErrorCode.TypeNotFound.rawValue
|
||||
let message = internalErrorMessage(forCode: code)
|
||||
return (nil, NSError(domain: LocksmithErrorDomain, code: code, userInfo: ["message": message]))
|
||||
}
|
||||
}
|
||||
|
||||
private class func performUpdate(request: CFDictionaryRef, inout result: AnyObject?) -> OSStatus {
|
||||
// We perform updates to the keychain by first deleting the matching object, then writing to it with the new value.
|
||||
SecItemDelete(request)
|
||||
// Even if the delete request failed (e.g. if the item didn't exist before), still try to save the new item.
|
||||
// If we get an error saving, we'll tell the user about it.
|
||||
|
||||
var status: OSStatus = withUnsafeMutablePointer(&result) { SecItemAdd(request, UnsafeMutablePointer($0)) }
|
||||
return status
|
||||
}
|
||||
|
||||
// MARK: Error Lookup
|
||||
enum ErrorMessage: String {
|
||||
case Allocate = "Failed to allocate memory."
|
||||
case AuthFailed = "Authorization/Authentication failed."
|
||||
case Decode = "Unable to decode the provided data."
|
||||
case Duplicate = "The item already exists."
|
||||
case InteractionNotAllowed = "Interaction with the Security Server is not allowed."
|
||||
case NoError = "No error."
|
||||
case NotAvailable = "No trust results are available."
|
||||
case NotFound = "The item cannot be found."
|
||||
case Param = "One or more parameters passed to the function were not valid."
|
||||
case Unimplemented = "Function or operation not implemented."
|
||||
}
|
||||
|
||||
enum LocksmithErrorCode: Int {
|
||||
case RequestNotSet = 1
|
||||
case TypeNotFound = 2
|
||||
case UnableToClear = 3
|
||||
}
|
||||
|
||||
enum LocksmithErrorMessage: String {
|
||||
case RequestNotSet = "keychainRequest was not set."
|
||||
case TypeNotFound = "The type of request given was undefined."
|
||||
case UnableToClear = "Unable to clear the keychain"
|
||||
}
|
||||
|
||||
class func keychainError(forCode statusCode: Int) -> NSError? {
|
||||
var error: NSError?
|
||||
|
||||
if statusCode != Int(errSecSuccess) {
|
||||
let message = errorMessage(statusCode)
|
||||
// println("Keychain request failed. Code: \(statusCode). Message: \(message)")
|
||||
error = NSError(domain: LocksmithErrorDomain, code: statusCode, userInfo: ["message": message])
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
// MARK: Private methods
|
||||
|
||||
private class func internalErrorMessage(forCode statusCode: Int) -> NSString {
|
||||
switch statusCode {
|
||||
case LocksmithErrorCode.RequestNotSet.rawValue:
|
||||
return LocksmithErrorMessage.RequestNotSet.rawValue
|
||||
case LocksmithErrorCode.UnableToClear.rawValue:
|
||||
return LocksmithErrorMessage.UnableToClear.rawValue
|
||||
default:
|
||||
return "Error message for code \(statusCode) not set"
|
||||
}
|
||||
}
|
||||
|
||||
private class func parseRequest(request: LocksmithRequest) -> NSMutableDictionary {
|
||||
var parsedRequest = NSMutableDictionary()
|
||||
|
||||
var options = [String: AnyObject?]()
|
||||
options[String(kSecAttrAccount)] = request.userAccount
|
||||
options[String(kSecAttrAccessGroup)] = request.group
|
||||
options[String(kSecAttrService)] = request.service
|
||||
options[String(kSecAttrSynchronizable)] = request.synchronizable
|
||||
options[String(kSecClass)] = securityCode(request.securityClass)
|
||||
|
||||
for (key, option) in options {
|
||||
parsedRequest.setOptional(option, forKey: key)
|
||||
}
|
||||
|
||||
switch request.type {
|
||||
case .Create:
|
||||
parsedRequest = parseCreateRequest(request, inDictionary: parsedRequest)
|
||||
case .Delete:
|
||||
parsedRequest = parseDeleteRequest(request, inDictionary: parsedRequest)
|
||||
case .Update:
|
||||
parsedRequest = parseCreateRequest(request, inDictionary: parsedRequest)
|
||||
default: // case .Read:
|
||||
parsedRequest = parseReadRequest(request, inDictionary: parsedRequest)
|
||||
}
|
||||
|
||||
return parsedRequest
|
||||
}
|
||||
|
||||
private class func parseCreateRequest(request: LocksmithRequest, inDictionary dictionary: NSMutableDictionary) -> NSMutableDictionary {
|
||||
|
||||
if let data = request.data {
|
||||
let encodedData = NSKeyedArchiver.archivedDataWithRootObject(data)
|
||||
dictionary.setObject(encodedData, forKey: String(kSecValueData))
|
||||
}
|
||||
|
||||
return dictionary
|
||||
}
|
||||
|
||||
|
||||
private class func parseReadRequest(request: LocksmithRequest, inDictionary dictionary: NSMutableDictionary) -> NSMutableDictionary {
|
||||
dictionary.setOptional(kCFBooleanTrue, forKey: String(kSecReturnData))
|
||||
|
||||
switch request.matchLimit {
|
||||
case .One:
|
||||
dictionary.setObject(kSecMatchLimitOne, forKey: String(kSecMatchLimit))
|
||||
case .Many:
|
||||
dictionary.setObject(kSecMatchLimitAll, forKey: String(kSecMatchLimit))
|
||||
}
|
||||
|
||||
return dictionary
|
||||
}
|
||||
|
||||
private class func parseDeleteRequest(request: LocksmithRequest, inDictionary dictionary: NSMutableDictionary) -> NSMutableDictionary {
|
||||
return dictionary
|
||||
}
|
||||
|
||||
private class func errorMessage(code: Int) -> NSString {
|
||||
switch code {
|
||||
case Int(errSecAllocate):
|
||||
return ErrorMessage.Allocate.rawValue
|
||||
case Int(errSecAuthFailed):
|
||||
return ErrorMessage.AuthFailed.rawValue
|
||||
case Int(errSecDecode):
|
||||
return ErrorMessage.Decode.rawValue
|
||||
case Int(errSecDuplicateItem):
|
||||
return ErrorMessage.Duplicate.rawValue
|
||||
case Int(errSecInteractionNotAllowed):
|
||||
return ErrorMessage.InteractionNotAllowed.rawValue
|
||||
case Int(errSecItemNotFound):
|
||||
return ErrorMessage.NotFound.rawValue
|
||||
case Int(errSecNotAvailable):
|
||||
return ErrorMessage.NotAvailable.rawValue
|
||||
case Int(errSecParam):
|
||||
return ErrorMessage.Param.rawValue
|
||||
case Int(errSecSuccess):
|
||||
return ErrorMessage.NoError.rawValue
|
||||
case Int(errSecUnimplemented):
|
||||
return ErrorMessage.Unimplemented.rawValue
|
||||
default:
|
||||
return "Undocumented error with code \(code)."
|
||||
}
|
||||
}
|
||||
|
||||
private class func securityCode(securityClass: SecurityClass) -> CFStringRef {
|
||||
switch securityClass {
|
||||
case .GenericPassword:
|
||||
return kSecClassGenericPassword
|
||||
case .Certificate:
|
||||
return kSecClassCertificate
|
||||
case .Identity:
|
||||
return kSecClassIdentity
|
||||
case .InternetPassword:
|
||||
return kSecClassInternetPassword
|
||||
case .Key:
|
||||
return kSecClassKey
|
||||
default:
|
||||
return kSecClassGenericPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Convenient Class Methods
|
||||
extension Locksmith {
|
||||
public class func saveData(data: Dictionary<String, String>, inService service: String, forUserAccount userAccount: String) -> NSError? {
|
||||
let saveRequest = LocksmithRequest(service: service, userAccount: userAccount, requestType: .Create, data: data)
|
||||
let (dictionary, error) = Locksmith.performRequest(saveRequest)
|
||||
return error
|
||||
}
|
||||
|
||||
public class func loadDataInService(service: String, forUserAccount userAccount: String) -> (NSDictionary?, NSError?) {
|
||||
let readRequest = LocksmithRequest(service: service, userAccount: userAccount)
|
||||
return Locksmith.performRequest(readRequest)
|
||||
}
|
||||
|
||||
public class func deleteDataInService(service: String, forUserAccount userAccount: String) -> NSError? {
|
||||
let deleteRequest = LocksmithRequest(service: service, userAccount: userAccount, requestType: .Delete)
|
||||
let (dictionary, error) = Locksmith.performRequest(deleteRequest)
|
||||
return error
|
||||
}
|
||||
|
||||
public class func updateData(data: Dictionary<String, String>, inService service: String, forUserAccount userAccount: String) -> NSError? {
|
||||
let updateRequest = LocksmithRequest(service: service, userAccount: userAccount, requestType: .Update, data: data)
|
||||
let (dictionary, error) = Locksmith.performRequest(updateRequest)
|
||||
return error
|
||||
}
|
||||
|
||||
public class func clearKeychain() -> NSError? {
|
||||
// Delete all of the keychain data of the given class
|
||||
func deleteDataForSecClass(secClass: CFTypeRef) -> NSError? {
|
||||
var request = NSMutableDictionary()
|
||||
request.setObject(secClass, forKey: String(kSecClass))
|
||||
|
||||
var status: OSStatus? = SecItemDelete(request as CFDictionaryRef)
|
||||
|
||||
if let status = status {
|
||||
var statusCode = Int(status)
|
||||
return Locksmith.keychainError(forCode: statusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// For each of the sec class types, delete all of the saved items of that type
|
||||
let classes = [kSecClassGenericPassword, kSecClassInternetPassword, kSecClassCertificate, kSecClassKey, kSecClassIdentity]
|
||||
|
||||
let errors: [NSError?] = classes.map({
|
||||
return deleteDataForSecClass($0)
|
||||
})
|
||||
|
||||
// Remove those that were successful, or failed with an acceptable error code
|
||||
let filtered = errors.filter({
|
||||
if let error = $0 {
|
||||
// There was an error
|
||||
// If the error indicates that there was no item with that sec class, that's fine.
|
||||
// Some of the sec classes will have nothing in them in most cases.
|
||||
return error.code != Int(errSecItemNotFound) ? true : false
|
||||
}
|
||||
|
||||
// There was no error
|
||||
return false
|
||||
})
|
||||
|
||||
// If the filtered array is empty, then everything went OK
|
||||
if filtered.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
// At least one of the delete operations failed
|
||||
let code = LocksmithErrorCode.UnableToClear.rawValue
|
||||
let message = internalErrorMessage(forCode: code)
|
||||
return NSError(domain: LocksmithErrorDomain, code: code, userInfo: ["message": message])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Dictionary Extensions
|
||||
extension NSMutableDictionary {
|
||||
func setOptional(optional: AnyObject?, forKey key: NSCopying) {
|
||||
if let object: AnyObject = optional {
|
||||
self.setObject(object, forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
//
|
||||
// LocksmithRequest.swift
|
||||
// Locksmith-Demo
|
||||
//
|
||||
// Created by Matthew Palmer on 26/10/2014.
|
||||
// Copyright (c) 2014 Colour Coding. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public enum SecurityClass: Int {
|
||||
case GenericPassword, InternetPassword, Certificate, Key, Identity
|
||||
}
|
||||
|
||||
public enum MatchLimit: Int {
|
||||
case One, Many
|
||||
}
|
||||
|
||||
public enum RequestType: Int {
|
||||
case Create, Read, Update, Delete
|
||||
}
|
||||
|
||||
public class LocksmithRequest: NSObject, DebugPrintable {
|
||||
// Keychain Options
|
||||
// Required
|
||||
var service: String
|
||||
var userAccount: String
|
||||
var type: RequestType = .Read // Default to non-destructive
|
||||
|
||||
// Optional
|
||||
var securityClass: SecurityClass = .GenericPassword // Default to password lookup
|
||||
var group: String?
|
||||
var data: NSDictionary?
|
||||
var matchLimit: MatchLimit = .One
|
||||
var synchronizable = false
|
||||
|
||||
// Debugging
|
||||
override public var debugDescription: String {
|
||||
return "service: \(self.service), type: \(self.type.rawValue), userAccount: \(self.userAccount)"
|
||||
}
|
||||
|
||||
required public init(service: String, userAccount: String) {
|
||||
self.service = service
|
||||
self.userAccount = userAccount
|
||||
}
|
||||
|
||||
convenience init(service: String, userAccount: String, requestType: RequestType) {
|
||||
self.init(service: service, userAccount: userAccount)
|
||||
self.type = requestType
|
||||
}
|
||||
|
||||
convenience init(service: String, userAccount: String, requestType: RequestType, data: NSDictionary) {
|
||||
self.init(service: service, userAccount: userAccount, requestType: requestType)
|
||||
self.data = data
|
||||
}
|
||||
}
|
||||
@@ -1,108 +1,242 @@
|
||||
//
|
||||
// LocksmithTests.swift
|
||||
// LocksmithTests
|
||||
// Locksmith.swift
|
||||
//
|
||||
// Created by Michael Hahn on 12/22/14.
|
||||
// Copyright (c) 2014 Mathew Palmer. All rights reserved.
|
||||
// Created by Matthew Palmer on 26/10/2014.
|
||||
// Copyright (c) 2014 Colour Coding. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreFoundation
|
||||
import UIKit
|
||||
import XCTest
|
||||
import Locksmith
|
||||
import Security
|
||||
|
||||
class LocksmithTests: XCTestCase {
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
Locksmith.clearKeychain()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
super.tearDown()
|
||||
}
|
||||
public let LocksmithDefaultService = NSBundle.mainBundle().infoDictionary![String(kCFBundleIdentifierKey)] as? String ?? "com.locksmith.defaultService"
|
||||
|
||||
// public class func saveData(data: Dictionary<String, String>, inService service: String, forUserAccount userAccount: String) -> NSError?
|
||||
func testSaveData_Once() {
|
||||
var error = Locksmith.saveData(["key": "value"], inService: "myService", forUserAccount: "myUserAccount")
|
||||
XCTAssert(error == nil, "❌: saving data")
|
||||
}
|
||||
// MARK: Locksmith Error
|
||||
public enum LocksmithError: String, ErrorType {
|
||||
case Allocate = "Failed to allocate memory."
|
||||
case AuthFailed = "Authorization/Authentication failed."
|
||||
case Decode = "Unable to decode the provided data."
|
||||
case Duplicate = "The item already exists."
|
||||
case InteractionNotAllowed = "Interaction with the Security Server is not allowed."
|
||||
case NoError = "No error."
|
||||
case NotAvailable = "No trust results are available."
|
||||
case NotFound = "The item cannot be found."
|
||||
case Param = "One or more parameters passed to the function were not valid."
|
||||
case RequestNotSet = "The request was not set"
|
||||
case TypeNotFound = "The type was not found"
|
||||
case UnableToClear = "Unable to clear the keychain"
|
||||
case Undefined = "An undefined error occurred"
|
||||
case Unimplemented = "Function or operation not implemented."
|
||||
|
||||
func testSaveData_Multiple() {
|
||||
var errors: [NSError?] = []
|
||||
for i in 0...10 {
|
||||
errors.append(Locksmith.saveData(["key": "value \(i)"], inService: "myService", forUserAccount: "myAccount\(i)"))
|
||||
}
|
||||
XCTAssert(errors.filter({ $0 != nil }).isEmpty, "❌: saving multiple items")
|
||||
}
|
||||
|
||||
func testSaveData_Duplicate() {
|
||||
// Should be successful
|
||||
let error1 = Locksmith.saveData(["key": "value"], inService: "myService", forUserAccount: "user")
|
||||
|
||||
// Should fail
|
||||
let error2 = Locksmith.saveData(["key": "value"], inService: "myService", forUserAccount: "user")
|
||||
|
||||
XCTAssert(error1 == nil && error2 != nil, "❌: saving duplicate data")
|
||||
}
|
||||
|
||||
// Setup the keychain for requests that use pre-existing values on the keychain (update, read, delete)
|
||||
func setupLoads() {
|
||||
Locksmith.saveData(["key": "value"], inService: "myService", forUserAccount: "user1")
|
||||
Locksmith.saveData(["anotherkey": "anothervalue"], inService: "myService", forUserAccount: "user2")
|
||||
Locksmith.saveData(["word": "definition"], inService: "myService", forUserAccount: "user3")
|
||||
}
|
||||
|
||||
// public class func loadDataInService(service: String, forUserAccount userAccount: String) -> (NSDictionary?, NSError?)
|
||||
func testLoadData_Once() {
|
||||
setupLoads()
|
||||
|
||||
let (dictionary, error) = Locksmith.loadDataInService("myService", forUserAccount: "user1")
|
||||
XCTAssert(dictionary!.valueForKey("key")! as NSString == "value" && error == nil, "❌: loading one item")
|
||||
}
|
||||
|
||||
func testLoadData_Multiple() {
|
||||
setupLoads()
|
||||
|
||||
let (dictionary, error) = Locksmith.loadDataInService("myService", forUserAccount: "user1")
|
||||
let (dictionary2, error2) = Locksmith.loadDataInService("myService", forUserAccount: "user2")
|
||||
let (dictionary3, error3) = Locksmith.loadDataInService("myService", forUserAccount: "user3")
|
||||
|
||||
XCTAssert(dictionary!.valueForKey("key")! as NSString == "value" && error == nil, "❌: loading multiple items")
|
||||
XCTAssert(dictionary2!.valueForKey("anotherkey")! as NSString == "anothervalue" && error == nil, "❌: loading multiple items")
|
||||
XCTAssert(dictionary3!.valueForKey("word")! as NSString == "definition" && error == nil, "❌: loading multiple items")
|
||||
}
|
||||
|
||||
// public class func updateData(data: Dictionary<String, String>, inService service: String, forUserAccount userAccount: String) -> NSError?
|
||||
func testUpdateData() {
|
||||
setupLoads()
|
||||
|
||||
let error = Locksmith.updateData(["key": "newvalue"], inService: "myService", forUserAccount: "user1")
|
||||
let (dictionary, err) = Locksmith.loadDataInService("myService", forUserAccount: "user1")
|
||||
XCTAssert(dictionary!.valueForKey("key")! as NSString == "newvalue" && error == nil, "❌: updating item")
|
||||
|
||||
// Updating an item that doesn't exist should create that item (i.e. performs a regular create request)
|
||||
let error2 = Locksmith.updateData(["key": "anothervalue"], inService: "myService", forUserAccount: "user1")
|
||||
XCTAssert(error2 == nil, "❌: updating item that doesn't exist")
|
||||
}
|
||||
|
||||
// public class func deleteDataInService(service: String, forUserAccount userAccount: String) -> NSError?
|
||||
func testDeleteData() {
|
||||
setupLoads()
|
||||
|
||||
let error = Locksmith.deleteDataInService("myService", forUserAccount: "user1")
|
||||
XCTAssert(error == nil, "❌: deleting existing item")
|
||||
|
||||
let error2 = Locksmith.deleteDataInService("myService", forUserAccount: "user1")
|
||||
XCTAssert(error2 != nil, "❌: deleting non existent item")
|
||||
}
|
||||
|
||||
func testPerformanceExample() {
|
||||
// This is an example of a performance test case.
|
||||
self.measureBlock() {
|
||||
// Put the code you want to measure the time of here.
|
||||
init?(fromStatusCode code: Int) {
|
||||
switch code {
|
||||
case Int(errSecAllocate):
|
||||
self = Allocate
|
||||
case Int(errSecAuthFailed):
|
||||
self = AuthFailed
|
||||
case Int(errSecDecode):
|
||||
self = Decode
|
||||
case Int(errSecDuplicateItem):
|
||||
self = Duplicate
|
||||
case Int(errSecInteractionNotAllowed):
|
||||
self = InteractionNotAllowed
|
||||
case Int(errSecItemNotFound):
|
||||
self = NotFound
|
||||
case Int(errSecNotAvailable):
|
||||
self = NotAvailable
|
||||
case Int(errSecParam):
|
||||
self = Param
|
||||
case Int(errSecUnimplemented):
|
||||
self = Unimplemented
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Locksmith
|
||||
public class Locksmith: NSObject {
|
||||
// MARK: Perform request
|
||||
public class func performRequest(request: LocksmithRequest) throws -> NSDictionary {
|
||||
let type = request.type
|
||||
var result: AnyObject?
|
||||
var status: OSStatus?
|
||||
|
||||
let parsedRequest: NSMutableDictionary = parseRequest(request)
|
||||
|
||||
let requestReference = parsedRequest as CFDictionaryRef
|
||||
|
||||
switch type {
|
||||
case .Create:
|
||||
status = withUnsafeMutablePointer(&result) { SecItemAdd(requestReference, UnsafeMutablePointer($0)) }
|
||||
case .Read:
|
||||
status = withUnsafeMutablePointer(&result) { SecItemCopyMatching(requestReference, UnsafeMutablePointer($0)) }
|
||||
case .Delete:
|
||||
status = SecItemDelete(requestReference)
|
||||
case .Update:
|
||||
status = Locksmith.performUpdate(requestReference, result: &result)
|
||||
}
|
||||
|
||||
guard let unwrappedStatus = status else {
|
||||
throw LocksmithError.TypeNotFound
|
||||
}
|
||||
|
||||
let statusCode = Int(unwrappedStatus)
|
||||
if let error = LocksmithError(fromStatusCode: statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
var resultsDictionary: NSDictionary?
|
||||
|
||||
if result != nil && type == .Read && status == errSecSuccess {
|
||||
if let data = result as? NSData {
|
||||
// Convert the retrieved data to a dictionary
|
||||
resultsDictionary = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? NSDictionary
|
||||
}
|
||||
}
|
||||
|
||||
return resultsDictionary ?? NSDictionary()
|
||||
}
|
||||
|
||||
// MARK: Private methods
|
||||
private class func performUpdate(request: CFDictionaryRef, inout result: AnyObject?) -> OSStatus {
|
||||
// We perform updates to the keychain by first deleting the matching object, then writing to it with the new value.
|
||||
SecItemDelete(request)
|
||||
|
||||
// Even if the delete request failed (e.g. if the item didn't exist before), still try to save the new item.
|
||||
// If we get an error saving, we'll tell the user about it.
|
||||
let status: OSStatus = withUnsafeMutablePointer(&result) { SecItemAdd(request, UnsafeMutablePointer($0)) }
|
||||
return status
|
||||
}
|
||||
|
||||
private class func parseRequest(request: LocksmithRequest) -> NSMutableDictionary {
|
||||
var parsedRequest = NSMutableDictionary()
|
||||
|
||||
var options = [String: AnyObject?]()
|
||||
options[String(kSecAttrAccount)] = request.userAccount
|
||||
options[String(kSecAttrAccessGroup)] = request.group
|
||||
options[String(kSecAttrService)] = request.service
|
||||
options[String(kSecAttrSynchronizable)] = request.synchronizable
|
||||
options[String(kSecClass)] = request.securityClass.rawValue
|
||||
|
||||
if let accessibleMode = request.accessible {
|
||||
options[String(kSecAttrAccessible)] = accessibleMode.rawValue
|
||||
}
|
||||
|
||||
for (key, option) in options {
|
||||
parsedRequest.setOptional(option, forKey: key)
|
||||
}
|
||||
|
||||
switch request.type {
|
||||
case .Create:
|
||||
parsedRequest = parseCreateRequest(request, inDictionary: parsedRequest)
|
||||
case .Delete:
|
||||
parsedRequest = parseDeleteRequest(request, inDictionary: parsedRequest)
|
||||
case .Update:
|
||||
parsedRequest = parseCreateRequest(request, inDictionary: parsedRequest)
|
||||
default: // case .Read:
|
||||
parsedRequest = parseReadRequest(request, inDictionary: parsedRequest)
|
||||
}
|
||||
|
||||
return parsedRequest
|
||||
}
|
||||
|
||||
private class func parseCreateRequest(request: LocksmithRequest, inDictionary dictionary: NSMutableDictionary) -> NSMutableDictionary {
|
||||
|
||||
if let data = request.data {
|
||||
let encodedData = NSKeyedArchiver.archivedDataWithRootObject(data)
|
||||
dictionary.setObject(encodedData, forKey: String(kSecValueData))
|
||||
}
|
||||
|
||||
return dictionary
|
||||
}
|
||||
|
||||
|
||||
private class func parseReadRequest(request: LocksmithRequest, inDictionary dictionary: NSMutableDictionary) -> NSMutableDictionary {
|
||||
dictionary.setOptional(kCFBooleanTrue, forKey: String(kSecReturnData))
|
||||
|
||||
switch request.matchLimit {
|
||||
case .One:
|
||||
dictionary.setObject(kSecMatchLimitOne, forKey: String(kSecMatchLimit))
|
||||
case .Many:
|
||||
dictionary.setObject(kSecMatchLimitAll, forKey: String(kSecMatchLimit))
|
||||
}
|
||||
|
||||
return dictionary
|
||||
}
|
||||
|
||||
private class func parseDeleteRequest(request: LocksmithRequest, inDictionary dictionary: NSMutableDictionary) -> NSMutableDictionary {
|
||||
return dictionary
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Convenient Class Methods
|
||||
extension Locksmith {
|
||||
public class func saveData(data: Dictionary<String, String>, forUserAccount userAccount: String, inService service: String = LocksmithDefaultService) throws {
|
||||
let saveRequest = LocksmithRequest(userAccount: userAccount, requestType: .Create, data: data, service: service)
|
||||
try Locksmith.performRequest(saveRequest)
|
||||
}
|
||||
|
||||
public class func loadDataForUserAccount(userAccount: String, inService service: String = LocksmithDefaultService) -> NSDictionary? {
|
||||
let readRequest = LocksmithRequest(userAccount: userAccount, service: service)
|
||||
|
||||
do {
|
||||
let dictionary = try Locksmith.performRequest(readRequest)
|
||||
return dictionary
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public class func deleteDataForUserAccount(userAccount: String, inService service: String = LocksmithDefaultService) throws {
|
||||
let deleteRequest = LocksmithRequest(userAccount: userAccount, requestType: .Delete, service: service)
|
||||
try Locksmith.performRequest(deleteRequest)
|
||||
}
|
||||
|
||||
public class func updateData(data: Dictionary<String, String>, forUserAccount userAccount: String, inService service: String = LocksmithDefaultService) throws {
|
||||
let updateRequest = LocksmithRequest(userAccount: userAccount, requestType: .Update, data: data, service: service)
|
||||
try Locksmith.performRequest(updateRequest)
|
||||
}
|
||||
|
||||
public class func clearKeychain() throws {
|
||||
// Delete all of the keychain data of the given class
|
||||
func deleteDataForSecClass(secClass: CFTypeRef) throws {
|
||||
let request = NSMutableDictionary()
|
||||
request.setObject(secClass, forKey: String(kSecClass))
|
||||
|
||||
let status: OSStatus? = SecItemDelete(request as CFDictionaryRef)
|
||||
|
||||
if let status = status {
|
||||
let statusCode = Int(status)
|
||||
if let error = LocksmithError(fromStatusCode: statusCode) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For each of the sec class types, delete all of the saved items of that type
|
||||
let classes = [kSecClassGenericPassword, kSecClassInternetPassword, kSecClassCertificate, kSecClassKey, kSecClassIdentity]
|
||||
|
||||
for classType in classes {
|
||||
do {
|
||||
try deleteDataForSecClass(classType)
|
||||
} catch let error as LocksmithError {
|
||||
// There was an error
|
||||
// If the error indicates that there was no item with that security class, that's fine.
|
||||
// Some of the sec classes will have nothing in them in most cases.
|
||||
if error != LocksmithError.NotFound {
|
||||
throw LocksmithError.UnableToClear
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Dictionary Extension
|
||||
extension NSMutableDictionary {
|
||||
func setOptional(optional: AnyObject?, forKey key: NSCopying) {
|
||||
if let object: AnyObject = optional {
|
||||
self.setObject(object, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import UIKit
|
||||
|
||||
public extension Dictionary {
|
||||
init(withoutOptionalValues initial: Dictionary<Key, Value?>) {
|
||||
self = [Key: Value]()
|
||||
for pair in initial {
|
||||
if pair.1 != nil {
|
||||
self[pair.0] = pair.1!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(pairs: [(Key, Value)]) {
|
||||
self = [Key: Value]()
|
||||
pairs.forEach { (k, v) -> () in
|
||||
self[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
init(initial: Dictionary<Key, Value>, toMerge: Dictionary<Key, Value>) {
|
||||
self = Dictionary<Key, Value>()
|
||||
|
||||
for pair in initial {
|
||||
self[pair.0] = pair.1
|
||||
}
|
||||
|
||||
for pair in toMerge {
|
||||
self[pair.0] = pair.1
|
||||
}
|
||||
}
|
||||
}
|
||||
+554
-259
@@ -1,303 +1,598 @@
|
||||
//
|
||||
// Locksmith.swift
|
||||
//
|
||||
// Created by Matthew Palmer on 26/10/2014.
|
||||
// Copyright (c) 2014 Colour Coding. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Security
|
||||
|
||||
public let LocksmithErrorDomain = "com.locksmith.error"
|
||||
public let LocksmithDefaultService = NSBundle.mainBundle().infoDictionary![String(kCFBundleIdentifierKey)] as? String ?? "com.locksmith.defaultService"
|
||||
|
||||
public class Locksmith: NSObject {
|
||||
// MARK: Perform request
|
||||
class func performRequest(request: LocksmithRequest) -> (NSDictionary?, NSError?) {
|
||||
let type = request.type
|
||||
//var result: Unmanaged<AnyObject>? = nil
|
||||
public typealias PerformRequestClosureType = (requestReference: CFDictionaryRef, inout result: AnyObject?) -> (OSStatus)
|
||||
|
||||
|
||||
// MARK: - Locksmith
|
||||
public struct Locksmith {
|
||||
public static func loadDataForUserAccount(userAccount: String, inService service: String = LocksmithDefaultService) -> [String: AnyObject]? {
|
||||
struct ReadRequest: GenericPasswordSecureStorable, ReadableSecureStorable {
|
||||
let service: String
|
||||
let account: String
|
||||
}
|
||||
|
||||
let request = ReadRequest(service: service, account: userAccount)
|
||||
return request.readFromSecureStore()?.data
|
||||
}
|
||||
|
||||
public static func saveData(data: [String: AnyObject], forUserAccount userAccount: String, inService service: String = LocksmithDefaultService) throws {
|
||||
struct CreateRequest: GenericPasswordSecureStorable, CreateableSecureStorable {
|
||||
let service: String
|
||||
let account: String
|
||||
let data: [String: AnyObject]
|
||||
}
|
||||
|
||||
let request = CreateRequest(service: service, account: userAccount, data: data)
|
||||
return try request.createInSecureStore()
|
||||
}
|
||||
|
||||
public static func deleteDataForUserAccount(userAccount: String, inService service: String = LocksmithDefaultService) throws {
|
||||
struct DeleteRequest: GenericPasswordSecureStorable, DeleteableSecureStorable {
|
||||
let service: String
|
||||
let account: String
|
||||
}
|
||||
|
||||
let request = DeleteRequest(service: service, account: userAccount)
|
||||
return try request.deleteFromSecureStore()
|
||||
}
|
||||
|
||||
public static func updateData(data: [String: AnyObject], forUserAccount userAccount: String, inService service: String = LocksmithDefaultService) throws {
|
||||
// Delete and then re-save
|
||||
do {
|
||||
try Locksmith.deleteDataForUserAccount(userAccount, inService: service)
|
||||
} catch {
|
||||
// Deletion is likely to fail if the piece of data doesn't exist yet.
|
||||
// This doesn't matter--we only tell the user about errors on the save request.
|
||||
}
|
||||
|
||||
return try Locksmith.saveData(data, forUserAccount: userAccount, inService: service)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SecureStorable
|
||||
/// The base protocol that indicates conforming types will have the ability to be stored in a secure storage container, such as the iOS keychain.
|
||||
public protocol SecureStorable {
|
||||
var accessible: LocksmithAccessibleOption? { get }
|
||||
var accessGroup: String? { get }
|
||||
}
|
||||
|
||||
public extension SecureStorable {
|
||||
var accessible: LocksmithAccessibleOption? { return nil }
|
||||
var accessGroup: String? { return nil }
|
||||
|
||||
var secureStorableBaseStoragePropertyDictionary: [String: AnyObject] {
|
||||
let dictionary = [
|
||||
String(kSecAttrAccessGroup): self.accessGroup,
|
||||
String(kSecAttrAccessible): self.accessible?.rawValue
|
||||
]
|
||||
|
||||
return Dictionary(withoutOptionalValues: dictionary)
|
||||
}
|
||||
|
||||
private func performSecureStorageAction(closure: PerformRequestClosureType, secureStoragePropertyDictionary: [String: AnyObject]) throws -> [String: AnyObject]? {
|
||||
var result: AnyObject?
|
||||
var status: OSStatus?
|
||||
let request = secureStoragePropertyDictionary
|
||||
let requestReference = request as CFDictionaryRef
|
||||
|
||||
var parsedRequest: NSMutableDictionary = parseRequest(request)
|
||||
let status = closure(requestReference: requestReference, result: &result)
|
||||
|
||||
var requestReference = parsedRequest as CFDictionaryRef
|
||||
let statusCode = Int(status)
|
||||
|
||||
switch type {
|
||||
case .Create:
|
||||
status = withUnsafeMutablePointer(&result) { SecItemAdd(requestReference, UnsafeMutablePointer($0)) }
|
||||
case .Read:
|
||||
status = withUnsafeMutablePointer(&result) { SecItemCopyMatching(requestReference, UnsafeMutablePointer($0)) }
|
||||
case .Delete:
|
||||
status = SecItemDelete(requestReference)
|
||||
case .Update:
|
||||
status = Locksmith.performUpdate(requestReference, result: &result)
|
||||
default:
|
||||
status = nil
|
||||
if let error = LocksmithError(fromStatusCode: statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if let status = status {
|
||||
var statusCode = Int(status)
|
||||
let error = Locksmith.keychainError(forCode: statusCode)
|
||||
var resultsDictionary: NSDictionary?
|
||||
|
||||
if result != nil {
|
||||
if type == .Read && status == errSecSuccess {
|
||||
|
||||
if let data = result as? NSData {
|
||||
// Convert the retrieved data to a dictionary
|
||||
resultsDictionary = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? NSDictionary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (resultsDictionary, error)
|
||||
} else {
|
||||
let code = LocksmithErrorCode.TypeNotFound.rawValue
|
||||
let message = internalErrorMessage(forCode: code)
|
||||
return (nil, NSError(domain: LocksmithErrorDomain, code: code, userInfo: ["message": message]))
|
||||
}
|
||||
}
|
||||
|
||||
private class func performUpdate(request: CFDictionaryRef, inout result: AnyObject?) -> OSStatus {
|
||||
// We perform updates to the keychain by first deleting the matching object, then writing to it with the new value.
|
||||
SecItemDelete(request)
|
||||
// Even if the delete request failed (e.g. if the item didn't exist before), still try to save the new item.
|
||||
// If we get an error saving, we'll tell the user about it.
|
||||
|
||||
var status: OSStatus = withUnsafeMutablePointer(&result) { SecItemAdd(request, UnsafeMutablePointer($0)) }
|
||||
return status
|
||||
}
|
||||
|
||||
// MARK: Error Lookup
|
||||
enum ErrorMessage: String {
|
||||
case Allocate = "Failed to allocate memory."
|
||||
case AuthFailed = "Authorization/Authentication failed."
|
||||
case Decode = "Unable to decode the provided data."
|
||||
case Duplicate = "The item already exists."
|
||||
case InteractionNotAllowed = "Interaction with the Security Server is not allowed."
|
||||
case NoError = "No error."
|
||||
case NotAvailable = "No trust results are available."
|
||||
case NotFound = "The item cannot be found."
|
||||
case Param = "One or more parameters passed to the function were not valid."
|
||||
case Unimplemented = "Function or operation not implemented."
|
||||
}
|
||||
|
||||
enum LocksmithErrorCode: Int {
|
||||
case RequestNotSet = 1
|
||||
case TypeNotFound = 2
|
||||
case UnableToClear = 3
|
||||
}
|
||||
|
||||
enum LocksmithErrorMessage: String {
|
||||
case RequestNotSet = "keychainRequest was not set."
|
||||
case TypeNotFound = "The type of request given was undefined."
|
||||
case UnableToClear = "Unable to clear the keychain"
|
||||
}
|
||||
|
||||
class func keychainError(forCode statusCode: Int) -> NSError? {
|
||||
var error: NSError?
|
||||
|
||||
if statusCode != Int(errSecSuccess) {
|
||||
let message = errorMessage(statusCode)
|
||||
// println("Keychain request failed. Code: \(statusCode). Message: \(message)")
|
||||
error = NSError(domain: LocksmithErrorDomain, code: statusCode, userInfo: ["message": message])
|
||||
// hmmmm... bit leaky
|
||||
if status != errSecSuccess {
|
||||
return nil
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
// MARK: Private methods
|
||||
|
||||
private class func internalErrorMessage(forCode statusCode: Int) -> NSString {
|
||||
switch statusCode {
|
||||
case LocksmithErrorCode.RequestNotSet.rawValue:
|
||||
return LocksmithErrorMessage.RequestNotSet.rawValue
|
||||
case LocksmithErrorCode.UnableToClear.rawValue:
|
||||
return LocksmithErrorMessage.UnableToClear.rawValue
|
||||
default:
|
||||
return "Error message for code \(statusCode) not set"
|
||||
}
|
||||
}
|
||||
|
||||
private class func parseRequest(request: LocksmithRequest) -> NSMutableDictionary {
|
||||
var parsedRequest = NSMutableDictionary()
|
||||
|
||||
var options = [String: AnyObject?]()
|
||||
options[String(kSecAttrAccount)] = request.userAccount
|
||||
options[String(kSecAttrAccessGroup)] = request.group
|
||||
options[String(kSecAttrService)] = request.service
|
||||
options[String(kSecAttrSynchronizable)] = request.synchronizable
|
||||
options[String(kSecClass)] = securityCode(request.securityClass)
|
||||
|
||||
for (key, option) in options {
|
||||
parsedRequest.setOptional(option, forKey: key)
|
||||
guard let dictionary = result as? NSDictionary else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch request.type {
|
||||
case .Create:
|
||||
parsedRequest = parseCreateRequest(request, inDictionary: parsedRequest)
|
||||
case .Delete:
|
||||
parsedRequest = parseDeleteRequest(request, inDictionary: parsedRequest)
|
||||
case .Update:
|
||||
parsedRequest = parseCreateRequest(request, inDictionary: parsedRequest)
|
||||
default: // case .Read:
|
||||
parsedRequest = parseReadRequest(request, inDictionary: parsedRequest)
|
||||
if dictionary[String(kSecValueData)] as? NSData == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return parsedRequest
|
||||
return result as? [String: AnyObject]
|
||||
}
|
||||
|
||||
private class func parseCreateRequest(request: LocksmithRequest, inDictionary dictionary: NSMutableDictionary) -> NSMutableDictionary {
|
||||
}
|
||||
|
||||
public extension SecureStorable where Self : InternetPasswordSecureStorable {
|
||||
private var internetPasswordBaseStoragePropertyDictionary: [String: AnyObject] {
|
||||
var dictionary = [String: AnyObject]()
|
||||
|
||||
if let data = request.data {
|
||||
let encodedData = NSKeyedArchiver.archivedDataWithRootObject(data)
|
||||
dictionary.setObject(encodedData, forKey: String(kSecValueData))
|
||||
// add in whatever turns out to be required...
|
||||
dictionary[String(kSecAttrServer)] = self.server
|
||||
dictionary[String(kSecAttrPort)] = self.port
|
||||
dictionary[String(kSecAttrProtocol)] = self.internetProtocol.rawValue
|
||||
dictionary[String(kSecAttrAuthenticationType)] = self.authenticationType.rawValue
|
||||
dictionary[String(kSecAttrSecurityDomain)] = self.securityDomain
|
||||
dictionary[String(kSecAttrPath)] = self.path
|
||||
dictionary[String(kSecClass)] = LocksmithSecurityClass.InternetPassword.rawValue
|
||||
|
||||
let toMergeWith = [
|
||||
self.accountSecureStoragePropertyDictionary,
|
||||
self.describableSecureStoragePropertyDictionary,
|
||||
self.commentableSecureStoragePropertyDictionary,
|
||||
self.creatorDesignatableSecureStoragePropertyDictionary,
|
||||
self.typeDesignatableSecureStoragePropertyDictionary,
|
||||
self.isInvisibleSecureStoragePropertyDictionary,
|
||||
self.isNegativeSecureStoragePropertyDictionary
|
||||
]
|
||||
|
||||
for dict in toMergeWith {
|
||||
dictionary = Dictionary(initial: dictionary, toMerge: dict)
|
||||
}
|
||||
|
||||
return dictionary
|
||||
}
|
||||
}
|
||||
|
||||
public protocol AccountBasedSecureStorable {
|
||||
/// The account that the stored value will belong to
|
||||
var account: String { get }
|
||||
}
|
||||
|
||||
public extension AccountBasedSecureStorable {
|
||||
private var accountSecureStoragePropertyDictionary: [String: AnyObject] {
|
||||
return [String(kSecAttrAccount): self.account]
|
||||
}
|
||||
}
|
||||
|
||||
public protocol AccountBasedSecureStorableResultType: AccountBasedSecureStorable, SecureStorableResultType {}
|
||||
|
||||
public extension AccountBasedSecureStorableResultType {
|
||||
var account: String {
|
||||
return self.resultDictionary[String(kSecAttrAccount)] as! String
|
||||
}
|
||||
}
|
||||
|
||||
public protocol DescribableSecureStorable {
|
||||
/// A description of the item in the secure storage container.
|
||||
var description: String? { get }
|
||||
}
|
||||
|
||||
public extension DescribableSecureStorable {
|
||||
var description: String? { return nil }
|
||||
|
||||
private var describableSecureStoragePropertyDictionary: [String: AnyObject] {
|
||||
return Dictionary(withoutOptionalValues: [
|
||||
String(kSecAttrDescription): self.description
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
public protocol DescribableSecureStorableResultType: DescribableSecureStorable, SecureStorableResultType {}
|
||||
|
||||
public extension DescribableSecureStorableResultType {
|
||||
var description: String? {
|
||||
return self.resultDictionary[String(kSecAttrDescription)] as? String
|
||||
}
|
||||
}
|
||||
|
||||
public protocol CommentableSecureStorable {
|
||||
/// A comment attached to the item in the secure storage container.
|
||||
var comment: String? { get }
|
||||
}
|
||||
|
||||
public extension CommentableSecureStorable {
|
||||
var comment: String? { return nil }
|
||||
|
||||
private class func parseReadRequest(request: LocksmithRequest, inDictionary dictionary: NSMutableDictionary) -> NSMutableDictionary {
|
||||
dictionary.setOptional(kCFBooleanTrue, forKey: String(kSecReturnData))
|
||||
private var commentableSecureStoragePropertyDictionary: [String: AnyObject] {
|
||||
return Dictionary(withoutOptionalValues: [
|
||||
String(kSecAttrComment): self.comment
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
public protocol CommentableSecureStorableResultType: CommentableSecureStorable, SecureStorableResultType {}
|
||||
|
||||
public extension CommentableSecureStorableResultType {
|
||||
var comment: String? {
|
||||
return self.resultDictionary[String(kSecAttrComment)] as? String
|
||||
}
|
||||
}
|
||||
|
||||
public protocol CreatorDesignatableSecureStorable {
|
||||
/// The creator of the item in the secure storage container.
|
||||
var creator: UInt? { get }
|
||||
}
|
||||
|
||||
public extension CreatorDesignatableSecureStorable {
|
||||
var creator: UInt? { return nil }
|
||||
|
||||
private var creatorDesignatableSecureStoragePropertyDictionary: [String: AnyObject] {
|
||||
return Dictionary(withoutOptionalValues: [String(kSecAttrCreator): self.creator])
|
||||
}
|
||||
}
|
||||
|
||||
public protocol CreatorDesignatableSecureStorableResultType: CreatorDesignatableSecureStorable, SecureStorableResultType {}
|
||||
|
||||
public extension CreatorDesignatableSecureStorableResultType {
|
||||
var creator: UInt? {
|
||||
return self.resultDictionary[String(kSecAttrCreator)] as? UInt
|
||||
}
|
||||
}
|
||||
|
||||
public protocol LabellableSecureStorable {
|
||||
/// A label for the item in the secure storage container.
|
||||
var label: String? { get }
|
||||
}
|
||||
|
||||
public extension LabellableSecureStorable {
|
||||
var label: String? { return nil }
|
||||
|
||||
private var labellableSecureStoragePropertyDictionary: [String: AnyObject] {
|
||||
return Dictionary(withoutOptionalValues: [String(kSecAttrLabel): self.label])
|
||||
}
|
||||
}
|
||||
|
||||
public protocol LabellableSecureStorableResultType: LabellableSecureStorable, SecureStorableResultType {}
|
||||
|
||||
public extension LabellableSecureStorableResultType {
|
||||
var label: String? {
|
||||
return self.resultDictionary[String(kSecAttrLabel)] as? String
|
||||
}
|
||||
}
|
||||
|
||||
public protocol TypeDesignatableSecureStorable {
|
||||
/// The type of the stored item
|
||||
var type: UInt? { get }
|
||||
}
|
||||
|
||||
public extension TypeDesignatableSecureStorable {
|
||||
var type: UInt? { return nil }
|
||||
|
||||
private var typeDesignatableSecureStoragePropertyDictionary: [String: AnyObject] {
|
||||
return Dictionary(withoutOptionalValues: [String(kSecAttrType): self.type])
|
||||
}
|
||||
}
|
||||
|
||||
public protocol TypeDesignatableSecureStorableResultType: TypeDesignatableSecureStorable, SecureStorableResultType {}
|
||||
|
||||
public extension TypeDesignatableSecureStorableResultType {
|
||||
var type: UInt? {
|
||||
return self.resultDictionary[String(kSecAttrType)] as? UInt
|
||||
}
|
||||
}
|
||||
|
||||
public protocol IsInvisibleAssignableSecureStorable {
|
||||
var isInvisible: Bool? { get }
|
||||
}
|
||||
|
||||
public extension IsInvisibleAssignableSecureStorable {
|
||||
var isInvisible: Bool? { return nil }
|
||||
|
||||
private var isInvisibleSecureStoragePropertyDictionary: [String: AnyObject] {
|
||||
return Dictionary(withoutOptionalValues: [String(kSecAttrIsInvisible): self.isInvisible])
|
||||
}
|
||||
}
|
||||
|
||||
public protocol IsInvisibleAssignableSecureStorableResultType: IsInvisibleAssignableSecureStorable, SecureStorableResultType {}
|
||||
|
||||
public extension IsInvisibleAssignableSecureStorableResultType {
|
||||
var isInvisible: Bool? {
|
||||
return self.resultDictionary[String(kSecAttrIsInvisible)] as? Bool
|
||||
}
|
||||
}
|
||||
|
||||
public protocol IsNegativeAssignableSecureStorable {
|
||||
var isNegative: Bool? { get }
|
||||
}
|
||||
|
||||
public extension IsNegativeAssignableSecureStorable {
|
||||
var isNegative: Bool? { return nil }
|
||||
|
||||
private var isNegativeSecureStoragePropertyDictionary: [String: AnyObject] {
|
||||
return Dictionary(withoutOptionalValues: [String(kSecAttrIsNegative): self.isNegative])
|
||||
}
|
||||
}
|
||||
|
||||
public protocol IsNegativeAssignableSecureStorableResultType: IsNegativeAssignableSecureStorable, SecureStorableResultType {
|
||||
}
|
||||
|
||||
public extension IsNegativeAssignableSecureStorableResultType {
|
||||
var isNegative: Bool? {
|
||||
return self.resultDictionary[String(kSecAttrIsNegative)] as? Bool
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - GenericPasswordSecureStorable
|
||||
/// The protocol that indicates a type conforms to the requirements of a generic password item in a secure storage container.
|
||||
/// Generic passwords are the most common types of things that are stored securely.
|
||||
public protocol GenericPasswordSecureStorable: AccountBasedSecureStorable, DescribableSecureStorable, CommentableSecureStorable, CreatorDesignatableSecureStorable, LabellableSecureStorable, TypeDesignatableSecureStorable, IsInvisibleAssignableSecureStorable, IsNegativeAssignableSecureStorable {
|
||||
|
||||
/// The service to which the type belongs
|
||||
var service: String { get }
|
||||
|
||||
// Optional properties
|
||||
var generic: NSData? { get }
|
||||
}
|
||||
|
||||
// Add extension to allow for optional properties in protocol
|
||||
public extension GenericPasswordSecureStorable {
|
||||
var generic: NSData? { return nil}
|
||||
}
|
||||
|
||||
// dear god what have i done...
|
||||
public protocol GenericPasswordSecureStorableResultType: GenericPasswordSecureStorable, SecureStorableResultType, AccountBasedSecureStorableResultType, DescribableSecureStorableResultType, CommentableSecureStorableResultType, CreatorDesignatableSecureStorableResultType, LabellableSecureStorableResultType, TypeDesignatableSecureStorableResultType, IsInvisibleAssignableSecureStorableResultType, IsNegativeAssignableSecureStorableResultType {}
|
||||
|
||||
public extension GenericPasswordSecureStorableResultType {
|
||||
var service: String {
|
||||
return self.resultDictionary[String(kSecAttrService)] as! String
|
||||
}
|
||||
|
||||
var generic: NSData? {
|
||||
return self.resultDictionary[String(kSecAttrGeneric)] as? NSData
|
||||
}
|
||||
}
|
||||
|
||||
public extension SecureStorable where Self : GenericPasswordSecureStorable {
|
||||
private var genericPasswordBaseStoragePropertyDictionary: [String: AnyObject] {
|
||||
var dictionary = [String: AnyObject?]()
|
||||
|
||||
switch request.matchLimit {
|
||||
case .One:
|
||||
dictionary.setObject(kSecMatchLimitOne, forKey: String(kSecMatchLimit))
|
||||
case .Many:
|
||||
dictionary.setObject(kSecMatchLimitAll, forKey: String(kSecMatchLimit))
|
||||
dictionary[String(kSecAttrService)] = self.service
|
||||
dictionary[String(kSecAttrGeneric)] = self.generic
|
||||
dictionary[String(kSecClass)] = LocksmithSecurityClass.GenericPassword.rawValue
|
||||
|
||||
dictionary = Dictionary(initial: dictionary, toMerge: self.describableSecureStoragePropertyDictionary)
|
||||
|
||||
let toMergeWith = [
|
||||
self.secureStorableBaseStoragePropertyDictionary,
|
||||
self.accountSecureStoragePropertyDictionary,
|
||||
self.describableSecureStoragePropertyDictionary,
|
||||
self.commentableSecureStoragePropertyDictionary,
|
||||
self.creatorDesignatableSecureStoragePropertyDictionary,
|
||||
self.typeDesignatableSecureStoragePropertyDictionary,
|
||||
self.labellableSecureStoragePropertyDictionary,
|
||||
self.isInvisibleSecureStoragePropertyDictionary,
|
||||
self.isNegativeSecureStoragePropertyDictionary
|
||||
]
|
||||
|
||||
for dict in toMergeWith {
|
||||
dictionary = Dictionary(initial: dictionary, toMerge: dict)
|
||||
}
|
||||
|
||||
return dictionary
|
||||
return Dictionary(withoutOptionalValues: dictionary)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - InternetPasswordSecureStorable
|
||||
/// A protocol that indicates a type conforms to the requirements of an internet password in a secure storage container.
|
||||
public protocol InternetPasswordSecureStorable: AccountBasedSecureStorable, DescribableSecureStorable, CommentableSecureStorable, CreatorDesignatableSecureStorable, TypeDesignatableSecureStorable, IsInvisibleAssignableSecureStorable, IsNegativeAssignableSecureStorable {
|
||||
var server: String { get }
|
||||
var port: Int { get }
|
||||
var internetProtocol: LocksmithInternetProtocol { get }
|
||||
var authenticationType: LocksmithInternetAuthenticationType { get }
|
||||
var securityDomain: String? { get }
|
||||
var path: String? { get }
|
||||
}
|
||||
|
||||
public extension InternetPasswordSecureStorable {
|
||||
var securityDomain: String? { return nil }
|
||||
var path: String? { return nil }
|
||||
}
|
||||
|
||||
public protocol InternetPasswordSecureStorableResultType: AccountBasedSecureStorableResultType, DescribableSecureStorableResultType, CommentableSecureStorableResultType, CreatorDesignatableSecureStorableResultType, TypeDesignatableSecureStorableResultType, IsInvisibleAssignableSecureStorableResultType, IsNegativeAssignableSecureStorableResultType {}
|
||||
|
||||
public extension InternetPasswordSecureStorableResultType {
|
||||
private func stringFromResultDictionary(key: CFString) -> String? {
|
||||
return self.resultDictionary[String(key)] as? String
|
||||
}
|
||||
|
||||
private class func parseDeleteRequest(request: LocksmithRequest, inDictionary dictionary: NSMutableDictionary) -> NSMutableDictionary {
|
||||
return dictionary
|
||||
var server: String {
|
||||
return stringFromResultDictionary(kSecAttrServer)!
|
||||
}
|
||||
|
||||
private class func errorMessage(code: Int) -> NSString {
|
||||
switch code {
|
||||
case Int(errSecAllocate):
|
||||
return ErrorMessage.Allocate.rawValue
|
||||
case Int(errSecAuthFailed):
|
||||
return ErrorMessage.AuthFailed.rawValue
|
||||
case Int(errSecDecode):
|
||||
return ErrorMessage.Decode.rawValue
|
||||
case Int(errSecDuplicateItem):
|
||||
return ErrorMessage.Duplicate.rawValue
|
||||
case Int(errSecInteractionNotAllowed):
|
||||
return ErrorMessage.InteractionNotAllowed.rawValue
|
||||
case Int(errSecItemNotFound):
|
||||
return ErrorMessage.NotFound.rawValue
|
||||
case Int(errSecNotAvailable):
|
||||
return ErrorMessage.NotAvailable.rawValue
|
||||
case Int(errSecParam):
|
||||
return ErrorMessage.Param.rawValue
|
||||
case Int(errSecSuccess):
|
||||
return ErrorMessage.NoError.rawValue
|
||||
case Int(errSecUnimplemented):
|
||||
return ErrorMessage.Unimplemented.rawValue
|
||||
default:
|
||||
return "Undocumented error with code \(code)."
|
||||
var port: Int {
|
||||
return self.resultDictionary[String(kSecAttrPort)] as! Int
|
||||
}
|
||||
|
||||
var internetProtocol: LocksmithInternetProtocol {
|
||||
return LocksmithInternetProtocol(rawValue: stringFromResultDictionary(kSecAttrProtocol)!)!
|
||||
}
|
||||
|
||||
var authenticationType: LocksmithInternetAuthenticationType {
|
||||
return LocksmithInternetAuthenticationType(rawValue: stringFromResultDictionary(kSecAttrAuthenticationType)!)!
|
||||
}
|
||||
|
||||
var securityDomain: String? {
|
||||
return stringFromResultDictionary(kSecAttrSecurityDomain)
|
||||
}
|
||||
|
||||
var path: String? {
|
||||
return stringFromResultDictionary(kSecAttrPath)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CertificateSecureStorable
|
||||
|
||||
public protocol CertificateSecureStorable: SecureStorable {}
|
||||
|
||||
// MARK: - KeySecureStorable
|
||||
|
||||
public protocol KeySecureStorable: SecureStorable {}
|
||||
|
||||
// MARK: - CreateableSecureStorable
|
||||
|
||||
/// Conformance to this protocol indicates that your type is able to be created and saved to a secure storage container.
|
||||
public protocol CreateableSecureStorable: SecureStorable {
|
||||
var data: [String: AnyObject] { get }
|
||||
var performCreateRequestClosure: PerformRequestClosureType { get }
|
||||
func createInSecureStore() throws
|
||||
}
|
||||
|
||||
// MARK: - ReadableSecureStorable
|
||||
/// Conformance to this protocol indicates that your type is able to be read from a secure storage container.
|
||||
public protocol ReadableSecureStorable: SecureStorable {
|
||||
var performReadRequestClosure: PerformRequestClosureType { get }
|
||||
func readFromSecureStore() -> SecureStorableResultType?
|
||||
}
|
||||
|
||||
public extension ReadableSecureStorable {
|
||||
var performReadRequestClosure: PerformRequestClosureType {
|
||||
return { (requestReference: CFDictionaryRef, inout result: AnyObject?) in
|
||||
return withUnsafeMutablePointer(&result) { SecItemCopyMatching(requestReference, UnsafeMutablePointer($0)) }
|
||||
}
|
||||
}
|
||||
|
||||
private class func securityCode(securityClass: SecurityClass) -> CFStringRef {
|
||||
switch securityClass {
|
||||
case .GenericPassword:
|
||||
return kSecClassGenericPassword
|
||||
case .Certificate:
|
||||
return kSecClassCertificate
|
||||
case .Identity:
|
||||
return kSecClassIdentity
|
||||
case .InternetPassword:
|
||||
return kSecClassInternetPassword
|
||||
case .Key:
|
||||
return kSecClassKey
|
||||
default:
|
||||
return kSecClassGenericPassword
|
||||
func readFromSecureStore() -> SecureStorableResultType? {
|
||||
// This must be implemented here so that we can properly override it in the type-specific implementations
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public extension ReadableSecureStorable where Self : GenericPasswordSecureStorable {
|
||||
var asReadableSecureStoragePropertyDictionary: [String: AnyObject] {
|
||||
var old = self.genericPasswordBaseStoragePropertyDictionary
|
||||
old[String(kSecReturnData)] = true
|
||||
old[String(kSecMatchLimit)] = kSecMatchLimitOne
|
||||
old[String(kSecReturnAttributes)] = kCFBooleanTrue
|
||||
|
||||
return old
|
||||
}
|
||||
}
|
||||
|
||||
public extension ReadableSecureStorable where Self : InternetPasswordSecureStorable {
|
||||
var asReadableSecureStoragePropertyDictionary: [String: AnyObject] {
|
||||
var old = self.internetPasswordBaseStoragePropertyDictionary
|
||||
old[String(kSecReturnData)] = true
|
||||
old[String(kSecMatchLimit)] = kSecMatchLimitOne
|
||||
old[String(kSecReturnAttributes)] = kCFBooleanTrue
|
||||
return old
|
||||
}
|
||||
}
|
||||
|
||||
struct GenericPasswordResult: GenericPasswordSecureStorableResultType {
|
||||
var resultDictionary: [String: AnyObject]
|
||||
}
|
||||
|
||||
public extension ReadableSecureStorable where Self : GenericPasswordSecureStorable {
|
||||
func readFromSecureStore() -> GenericPasswordSecureStorableResultType? {
|
||||
do {
|
||||
let result = try performSecureStorageAction(performReadRequestClosure, secureStoragePropertyDictionary: self.asReadableSecureStoragePropertyDictionary)
|
||||
return GenericPasswordResult(resultDictionary: result!)
|
||||
} catch {
|
||||
print(error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Convenient Class Methods
|
||||
extension Locksmith {
|
||||
public class func saveData(data: Dictionary<String, String>, inService service: String, forUserAccount userAccount: String) -> NSError? {
|
||||
let saveRequest = LocksmithRequest(service: service, userAccount: userAccount, requestType: .Create, data: data)
|
||||
let (dictionary, error) = Locksmith.performRequest(saveRequest)
|
||||
return error
|
||||
}
|
||||
|
||||
public class func loadDataInService(service: String, forUserAccount userAccount: String) -> (NSDictionary?, NSError?) {
|
||||
let readRequest = LocksmithRequest(service: service, userAccount: userAccount)
|
||||
return Locksmith.performRequest(readRequest)
|
||||
}
|
||||
|
||||
public class func deleteDataInService(service: String, forUserAccount userAccount: String) -> NSError? {
|
||||
let deleteRequest = LocksmithRequest(service: service, userAccount: userAccount, requestType: .Delete)
|
||||
let (dictionary, error) = Locksmith.performRequest(deleteRequest)
|
||||
return error
|
||||
}
|
||||
|
||||
public class func updateData(data: Dictionary<String, String>, inService service: String, forUserAccount userAccount: String) -> NSError? {
|
||||
let updateRequest = LocksmithRequest(service: service, userAccount: userAccount, requestType: .Update, data: data)
|
||||
let (dictionary, error) = Locksmith.performRequest(updateRequest)
|
||||
return error
|
||||
}
|
||||
|
||||
public class func clearKeychain() -> NSError? {
|
||||
// Delete all of the keychain data of the given class
|
||||
func deleteDataForSecClass(secClass: CFTypeRef) -> NSError? {
|
||||
var request = NSMutableDictionary()
|
||||
request.setObject(secClass, forKey: String(kSecClass))
|
||||
|
||||
var status: OSStatus? = SecItemDelete(request as CFDictionaryRef)
|
||||
|
||||
if let status = status {
|
||||
var statusCode = Int(status)
|
||||
return Locksmith.keychainError(forCode: statusCode)
|
||||
}
|
||||
|
||||
public extension ReadableSecureStorable where Self : InternetPasswordSecureStorable {
|
||||
func readFromSecureStore() -> InternetPasswordSecureStorableResultType? {
|
||||
do {
|
||||
let result = try performSecureStorageAction(performReadRequestClosure, secureStoragePropertyDictionary: self.asReadableSecureStoragePropertyDictionary)
|
||||
return InternetPasswordResult(resultDictionary: result!)
|
||||
} catch {
|
||||
print(error)
|
||||
return nil
|
||||
}
|
||||
|
||||
// For each of the sec class types, delete all of the saved items of that type
|
||||
let classes = [kSecClassGenericPassword, kSecClassInternetPassword, kSecClassCertificate, kSecClassKey, kSecClassIdentity]
|
||||
|
||||
let errors: [NSError?] = classes.map({
|
||||
return deleteDataForSecClass($0)
|
||||
})
|
||||
|
||||
// Remove those that were successful, or failed with an acceptable error code
|
||||
let filtered = errors.filter({
|
||||
if let error = $0 {
|
||||
// There was an error
|
||||
// If the error indicates that there was no item with that sec class, that's fine.
|
||||
// Some of the sec classes will have nothing in them in most cases.
|
||||
return error.code != Int(errSecItemNotFound) ? true : false
|
||||
}
|
||||
|
||||
// There was no error
|
||||
return false
|
||||
})
|
||||
|
||||
// If the filtered array is empty, then everything went OK
|
||||
if filtered.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
// At least one of the delete operations failed
|
||||
let code = LocksmithErrorCode.UnableToClear.rawValue
|
||||
let message = internalErrorMessage(forCode: code)
|
||||
return NSError(domain: LocksmithErrorDomain, code: code, userInfo: ["message": message])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Dictionary Extensions
|
||||
extension NSMutableDictionary {
|
||||
func setOptional(optional: AnyObject?, forKey key: NSCopying) {
|
||||
if let object: AnyObject = optional {
|
||||
self.setObject(object, forKey: key)
|
||||
|
||||
// MARK: - DeleteableSecureStorable
|
||||
/// Conformance to this protocol indicates that your type is able to be deleted from a secure storage container.
|
||||
public protocol DeleteableSecureStorable: SecureStorable {
|
||||
var performDeleteRequestClosure: PerformRequestClosureType { get }
|
||||
func deleteFromSecureStore() throws
|
||||
}
|
||||
|
||||
// MARK: - Default property dictionaries
|
||||
|
||||
public extension CreateableSecureStorable where Self : GenericPasswordSecureStorable {
|
||||
var asCreateableSecureStoragePropertyDictionary: [String: AnyObject] {
|
||||
var old = self.genericPasswordBaseStoragePropertyDictionary
|
||||
old[String(kSecValueData)] = NSKeyedArchiver.archivedDataWithRootObject(self.data)
|
||||
return old
|
||||
}
|
||||
}
|
||||
|
||||
public extension CreateableSecureStorable where Self : GenericPasswordSecureStorable {
|
||||
func createInSecureStore() throws {
|
||||
try performSecureStorageAction(performCreateRequestClosure, secureStoragePropertyDictionary: self.asCreateableSecureStoragePropertyDictionary)
|
||||
}
|
||||
}
|
||||
|
||||
public extension CreateableSecureStorable where Self : InternetPasswordSecureStorable {
|
||||
var asCreateableSecureStoragePropertyDictionary: [String: AnyObject] {
|
||||
var old = self.internetPasswordBaseStoragePropertyDictionary
|
||||
old[String(kSecValueData)] = NSKeyedArchiver.archivedDataWithRootObject(self.data)
|
||||
return old
|
||||
}
|
||||
}
|
||||
|
||||
public extension CreateableSecureStorable {
|
||||
var performCreateRequestClosure: PerformRequestClosureType {
|
||||
return { (requestReference: CFDictionaryRef, inout result: AnyObject?) in
|
||||
return withUnsafeMutablePointer(&result) { SecItemAdd(requestReference, UnsafeMutablePointer($0)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension CreateableSecureStorable where Self : InternetPasswordSecureStorable {
|
||||
func createInSecureStore() throws {
|
||||
try performSecureStorageAction(performCreateRequestClosure, secureStoragePropertyDictionary: self.asCreateableSecureStoragePropertyDictionary)
|
||||
}
|
||||
}
|
||||
|
||||
public extension DeleteableSecureStorable {
|
||||
var performDeleteRequestClosure: PerformRequestClosureType {
|
||||
return { (requestReference, _) in
|
||||
return SecItemDelete(requestReference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension DeleteableSecureStorable where Self : GenericPasswordSecureStorable {
|
||||
var asDeleteableSecureStoragePropertyDictionary: [String: AnyObject] {
|
||||
return self.genericPasswordBaseStoragePropertyDictionary
|
||||
}
|
||||
}
|
||||
|
||||
public extension DeleteableSecureStorable where Self : InternetPasswordSecureStorable {
|
||||
var asDeleteableSecureStoragePropertyDictionary: [String: AnyObject] {
|
||||
return self.internetPasswordBaseStoragePropertyDictionary
|
||||
}
|
||||
}
|
||||
|
||||
public extension DeleteableSecureStorable where Self : GenericPasswordSecureStorable {
|
||||
func deleteFromSecureStore() throws {
|
||||
try performSecureStorageAction(performDeleteRequestClosure, secureStoragePropertyDictionary: self.asDeleteableSecureStoragePropertyDictionary)
|
||||
}
|
||||
}
|
||||
|
||||
public extension DeleteableSecureStorable where Self : InternetPasswordSecureStorable {
|
||||
func deleteFromSecureStore() throws {
|
||||
try performSecureStorageAction(performDeleteRequestClosure, secureStoragePropertyDictionary: self.asDeleteableSecureStoragePropertyDictionary)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: ResultTypes
|
||||
public protocol SecureStorableResultType: SecureStorable {
|
||||
var resultDictionary: [String: AnyObject] { get }
|
||||
var data: [String: AnyObject]? { get }
|
||||
}
|
||||
|
||||
struct InternetPasswordResult: InternetPasswordSecureStorableResultType {
|
||||
var resultDictionary: [String: AnyObject]
|
||||
}
|
||||
|
||||
public extension SecureStorableResultType {
|
||||
var resultDictionary: [String: AnyObject] {
|
||||
return [String: AnyObject]()
|
||||
}
|
||||
|
||||
var data: [String: AnyObject]? {
|
||||
guard let aData = resultDictionary[String(kSecValueData)] as? NSData else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NSKeyedUnarchiver.unarchiveObjectWithData(aData) as? [String: AnyObject]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: Accessible
|
||||
public enum LocksmithAccessibleOption: RawRepresentable {
|
||||
case WhenUnlocked, AfterFirstUnlock, Always, WhenPasscodeSetThisDeviceOnly, WhenUnlockedThisDeviceOnly, AfterFirstUnlockThisDeviceOnly, AlwaysThisDeviceOnly
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case String(kSecAttrAccessibleWhenUnlocked):
|
||||
self = WhenUnlocked
|
||||
case String(kSecAttrAccessibleAfterFirstUnlock):
|
||||
self = AfterFirstUnlock
|
||||
case String(kSecAttrAccessibleAlways):
|
||||
self = Always
|
||||
case String(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly):
|
||||
self = WhenPasscodeSetThisDeviceOnly
|
||||
case String(kSecAttrAccessibleWhenUnlockedThisDeviceOnly):
|
||||
self = WhenUnlockedThisDeviceOnly
|
||||
case String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly):
|
||||
self = AfterFirstUnlockThisDeviceOnly
|
||||
case String(kSecAttrAccessibleAlwaysThisDeviceOnly):
|
||||
self = AlwaysThisDeviceOnly
|
||||
default:
|
||||
print("Accessible: invalid rawValue provided. Defaulting to Accessible.WhenUnlocked.")
|
||||
self = WhenUnlocked
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .WhenUnlocked:
|
||||
return String(kSecAttrAccessibleWhenUnlocked)
|
||||
case .AfterFirstUnlock:
|
||||
return String(kSecAttrAccessibleAfterFirstUnlock)
|
||||
case .Always:
|
||||
return String(kSecAttrAccessibleAlways)
|
||||
case .WhenPasscodeSetThisDeviceOnly:
|
||||
return String(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly)
|
||||
case .WhenUnlockedThisDeviceOnly:
|
||||
return String(kSecAttrAccessibleWhenUnlockedThisDeviceOnly)
|
||||
case .AfterFirstUnlockThisDeviceOnly:
|
||||
return String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)
|
||||
case .AlwaysThisDeviceOnly:
|
||||
return String(kSecAttrAccessibleAlwaysThisDeviceOnly)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: Locksmith Error
|
||||
public enum LocksmithError: String, ErrorType {
|
||||
case Allocate = "Failed to allocate memory."
|
||||
case AuthFailed = "Authorization/Authentication failed."
|
||||
case Decode = "Unable to decode the provided data."
|
||||
case Duplicate = "The item already exists."
|
||||
case InteractionNotAllowed = "Interaction with the Security Server is not allowed."
|
||||
case NoError = "No error."
|
||||
case NotAvailable = "No trust results are available."
|
||||
case NotFound = "The item cannot be found."
|
||||
case Param = "One or more parameters passed to the function were not valid."
|
||||
case RequestNotSet = "The request was not set"
|
||||
case TypeNotFound = "The type was not found"
|
||||
case UnableToClear = "Unable to clear the keychain"
|
||||
case Undefined = "An undefined error occurred"
|
||||
case Unimplemented = "Function or operation not implemented."
|
||||
|
||||
init?(fromStatusCode code: Int) {
|
||||
switch code {
|
||||
case Int(errSecAllocate):
|
||||
self = Allocate
|
||||
case Int(errSecAuthFailed):
|
||||
self = AuthFailed
|
||||
case Int(errSecDecode):
|
||||
self = Decode
|
||||
case Int(errSecDuplicateItem):
|
||||
self = Duplicate
|
||||
case Int(errSecInteractionNotAllowed):
|
||||
self = InteractionNotAllowed
|
||||
case Int(errSecItemNotFound):
|
||||
self = NotFound
|
||||
case Int(errSecNotAvailable):
|
||||
self = NotAvailable
|
||||
case Int(errSecParam):
|
||||
self = Param
|
||||
case Int(errSecUnimplemented):
|
||||
self = Unimplemented
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import UIKit
|
||||
|
||||
public enum LocksmithInternetAuthenticationType: RawRepresentable {
|
||||
case NTLM, MSN, DPA, RPA, HTTPBasic, HTTPDigest, HTMLForm, Default
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case String(kSecAttrAuthenticationTypeNTLM):
|
||||
self = NTLM
|
||||
case String(kSecAttrAuthenticationTypeMSN):
|
||||
self = MSN
|
||||
case String(kSecAttrAuthenticationTypeDPA):
|
||||
self = DPA
|
||||
case String(kSecAttrAuthenticationTypeRPA):
|
||||
self = RPA
|
||||
case String(kSecAttrAuthenticationTypeHTTPBasic):
|
||||
self = HTTPBasic
|
||||
case String(kSecAttrAuthenticationTypeHTTPDigest):
|
||||
self = HTTPDigest
|
||||
case String(kSecAttrAuthenticationTypeHTMLForm):
|
||||
self = HTMLForm
|
||||
case String(kSecAttrAuthenticationTypeDefault):
|
||||
self = Default
|
||||
default:
|
||||
self = Default
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .NTLM:
|
||||
return String(kSecAttrAuthenticationTypeNTLM)
|
||||
case .MSN:
|
||||
return String(kSecAttrAuthenticationTypeMSN)
|
||||
case .DPA:
|
||||
return String(kSecAttrAuthenticationTypeDPA)
|
||||
case .RPA:
|
||||
return String(kSecAttrAuthenticationTypeRPA)
|
||||
case .HTTPBasic:
|
||||
return String(kSecAttrAuthenticationTypeHTTPBasic)
|
||||
case .HTTPDigest:
|
||||
return String(kSecAttrAuthenticationTypeHTTPDigest)
|
||||
case .HTMLForm:
|
||||
return String(kSecAttrAuthenticationTypeHTMLForm)
|
||||
case .Default:
|
||||
return String(kSecAttrAuthenticationTypeDefault)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import UIKit
|
||||
|
||||
public enum LocksmithInternetProtocol: RawRepresentable {
|
||||
case FTP, FTPAccount, HTTP, IRC, NNTP, POP3, SMTP, SOCKS, IMAP, LDAP, AppleTalk, AFP, Telnet, SSH, FTPS, HTTPS, HTTPProxy, HTTPSProxy, FTPProxy, SMB, RTSP, RTSPProxy, DAAP, EPPC, IPP, NNTPS, LDAPS, TelnetS, IMAPS, IRCS, POP3S
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case String(kSecAttrProtocolFTP):
|
||||
self = FTP
|
||||
case String(kSecAttrProtocolFTPAccount):
|
||||
self = FTPAccount
|
||||
case String(kSecAttrProtocolHTTP):
|
||||
self = HTTP
|
||||
case String(kSecAttrProtocolIRC):
|
||||
self = IRC
|
||||
case String(kSecAttrProtocolNNTP):
|
||||
self = NNTP
|
||||
case String(kSecAttrProtocolPOP3):
|
||||
self = POP3
|
||||
case String(kSecAttrProtocolSMTP):
|
||||
self = SMTP
|
||||
case String(kSecAttrProtocolSOCKS):
|
||||
self = SOCKS
|
||||
case String(kSecAttrProtocolIMAP):
|
||||
self = IMAP
|
||||
case String(kSecAttrProtocolLDAP):
|
||||
self = LDAP
|
||||
case String(kSecAttrProtocolAppleTalk):
|
||||
self = AppleTalk
|
||||
case String(kSecAttrProtocolAFP):
|
||||
self = AFP
|
||||
case String(kSecAttrProtocolTelnet):
|
||||
self = Telnet
|
||||
case String(kSecAttrProtocolSSH):
|
||||
self = SSH
|
||||
case String(kSecAttrProtocolFTPS):
|
||||
self = FTPS
|
||||
case String(kSecAttrProtocolHTTPS):
|
||||
self = HTTPS
|
||||
case String(kSecAttrProtocolHTTPProxy):
|
||||
self = HTTPProxy
|
||||
case String(kSecAttrProtocolHTTPSProxy):
|
||||
self = HTTPSProxy
|
||||
case String(kSecAttrProtocolFTPProxy):
|
||||
self = FTPProxy
|
||||
case String(kSecAttrProtocolSMB):
|
||||
self = SMB
|
||||
case String(kSecAttrProtocolRTSP):
|
||||
self = RTSP
|
||||
case String(kSecAttrProtocolRTSPProxy):
|
||||
self = RTSPProxy
|
||||
case String(kSecAttrProtocolDAAP):
|
||||
self = DAAP
|
||||
case String(kSecAttrProtocolEPPC):
|
||||
self = EPPC
|
||||
case String(kSecAttrProtocolIPP):
|
||||
self = IPP
|
||||
case String(kSecAttrProtocolNNTPS):
|
||||
self = NNTPS
|
||||
case String(kSecAttrProtocolLDAPS):
|
||||
self = LDAPS
|
||||
case String(kSecAttrProtocolTelnetS):
|
||||
self = TelnetS
|
||||
case String(kSecAttrProtocolIMAPS):
|
||||
self = IMAPS
|
||||
case String(kSecAttrProtocolIRCS):
|
||||
self = IRCS
|
||||
case String(kSecAttrProtocolPOP3S):
|
||||
self = POP3S
|
||||
default:
|
||||
self = HTTP
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .FTP:
|
||||
return String(kSecAttrProtocolFTP)
|
||||
case .FTPAccount:
|
||||
return String(kSecAttrProtocolFTPAccount)
|
||||
case .HTTP:
|
||||
return String(kSecAttrProtocolHTTP)
|
||||
case .IRC:
|
||||
return String(kSecAttrProtocolIRC)
|
||||
case .NNTP:
|
||||
return String(kSecAttrProtocolNNTP)
|
||||
case .POP3:
|
||||
return String(kSecAttrProtocolPOP3)
|
||||
case .SMTP:
|
||||
return String(kSecAttrProtocolSMTP)
|
||||
case .SOCKS:
|
||||
return String(kSecAttrProtocolSOCKS)
|
||||
case .IMAP:
|
||||
return String(kSecAttrProtocolIMAP)
|
||||
case .LDAP:
|
||||
return String(kSecAttrProtocolLDAP)
|
||||
case .AppleTalk:
|
||||
return String(kSecAttrProtocolAppleTalk)
|
||||
case .AFP:
|
||||
return String(kSecAttrProtocolAFP)
|
||||
case .Telnet:
|
||||
return String(kSecAttrProtocolTelnet)
|
||||
case .SSH:
|
||||
return String(kSecAttrProtocolSSH)
|
||||
case .FTPS:
|
||||
return String(kSecAttrProtocolFTPS)
|
||||
case .HTTPS:
|
||||
return String(kSecAttrProtocolHTTPS)
|
||||
case .HTTPProxy:
|
||||
return String(kSecAttrProtocolHTTPProxy)
|
||||
case .HTTPSProxy:
|
||||
return String(kSecAttrProtocolHTTPSProxy)
|
||||
case .FTPProxy:
|
||||
return String(kSecAttrProtocolFTPProxy)
|
||||
case .SMB:
|
||||
return String(kSecAttrProtocolSMB)
|
||||
case .RTSP:
|
||||
return String(kSecAttrProtocolRTSP)
|
||||
case .RTSPProxy:
|
||||
return String(kSecAttrProtocolRTSPProxy)
|
||||
case .DAAP:
|
||||
return String(kSecAttrProtocolDAAP)
|
||||
case .EPPC:
|
||||
return String(kSecAttrProtocolEPPC)
|
||||
case .IPP:
|
||||
return String(kSecAttrProtocolIPP)
|
||||
case .NNTPS:
|
||||
return String(kSecAttrProtocolNNTPS)
|
||||
case .LDAPS:
|
||||
return String(kSecAttrProtocolLDAPS)
|
||||
case .TelnetS:
|
||||
return String(kSecAttrProtocolTelnetS)
|
||||
case .IMAPS:
|
||||
return String(kSecAttrProtocolIMAPS)
|
||||
case .IRCS:
|
||||
return String(kSecAttrProtocolIRCS)
|
||||
case .POP3S:
|
||||
return String(kSecAttrProtocolPOP3S)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
//
|
||||
// LocksmithRequest.swift
|
||||
//
|
||||
// Created by Matthew Palmer on 26/10/2014.
|
||||
// Copyright (c) 2014 Colour Coding. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Security
|
||||
|
||||
public enum SecurityClass: Int {
|
||||
case GenericPassword, InternetPassword, Certificate, Key, Identity
|
||||
}
|
||||
|
||||
public enum MatchLimit: Int {
|
||||
case One, Many
|
||||
}
|
||||
|
||||
public enum RequestType: Int {
|
||||
case Create, Read, Update, Delete
|
||||
}
|
||||
|
||||
public class LocksmithRequest: NSObject, DebugPrintable {
|
||||
// Keychain Options
|
||||
// Required
|
||||
var service: String
|
||||
var userAccount: String
|
||||
var type: RequestType = .Read // Default to non-destructive
|
||||
|
||||
// Optional
|
||||
var securityClass: SecurityClass = .GenericPassword // Default to password lookup
|
||||
var group: String?
|
||||
var data: NSDictionary?
|
||||
var matchLimit: MatchLimit = .One
|
||||
var synchronizable = false
|
||||
|
||||
// Debugging
|
||||
override public var debugDescription: String {
|
||||
return "service: \(self.service), type: \(self.type.rawValue), userAccount: \(self.userAccount)"
|
||||
}
|
||||
|
||||
required public init(service: String, userAccount: String) {
|
||||
self.service = service
|
||||
self.userAccount = userAccount
|
||||
}
|
||||
|
||||
convenience init(service: String, userAccount: String, requestType: RequestType) {
|
||||
self.init(service: service, userAccount: userAccount)
|
||||
self.type = requestType
|
||||
}
|
||||
|
||||
convenience init(service: String, userAccount: String, requestType: RequestType, data: NSDictionary) {
|
||||
self.init(service: service, userAccount: userAccount, requestType: requestType)
|
||||
self.data = data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import UIKit
|
||||
|
||||
// With thanks to http://iosdeveloperzone.com/2014/10/22/taming-foundation-constants-into-swift-enums/
|
||||
// MARK: Security Class
|
||||
public enum LocksmithSecurityClass: RawRepresentable {
|
||||
case GenericPassword, InternetPassword, Certificate, Key, Identity
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case String(kSecClassGenericPassword):
|
||||
self = GenericPassword
|
||||
case String(kSecClassInternetPassword):
|
||||
self = InternetPassword
|
||||
case String(kSecClassCertificate):
|
||||
self = Certificate
|
||||
case String(kSecClassKey):
|
||||
self = Key
|
||||
case String(kSecClassIdentity):
|
||||
self = Identity
|
||||
default:
|
||||
print("SecurityClass: Invalid raw value provided. Defaulting to .GenericPassword")
|
||||
self = GenericPassword
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .GenericPassword:
|
||||
return String(kSecClassGenericPassword)
|
||||
case .InternetPassword:
|
||||
return String(kSecClassInternetPassword)
|
||||
case .Certificate:
|
||||
return String(kSecClassCertificate)
|
||||
case .Key:
|
||||
return String(kSecClassKey)
|
||||
case .Identity:
|
||||
return String(kSecClassIdentity)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,21 +5,21 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.locksmith.$(PRODUCT_NAME:rfc1034identifier)</string>
|
||||
<string>com.matthewpalmer.${PRODUCT_NAME:rfc1034identifier}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>1.1.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<string>${CURRENT_PROJECT_VERSION}</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
@@ -1,87 +1,439 @@
|
||||
# Locksmith
|
||||
<h1><span style='color:red !important;'>Locksmith</span></h1>
|
||||
|
||||
A sane way to work with the iOS Keychain in Swift.
|
||||
A powerful, protocol-oriented library for working with the iOS Keychain in Swift.
|
||||
|
||||
**What makes Locksmith different to other keychain wrappers?**
|
||||
|
||||
* Locksmith’s API is both super-simple and deeply powerful
|
||||
* Provides access to all of the keychain’s metadata in a type-useful way via `ResultType` protocols—save an `NSDate`, get an `NSDate` back (without typecasting!)
|
||||
* Add functionality to your existing types for free
|
||||
* Useful enums and Swift-native types
|
||||
|
||||
<!--[](https://travis-ci.org/matthewpalmer/Locksmith)-->
|
||||
[](http://cocoadocs.org/docsets/Locksmith)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[](http://cocoadocs.org/docsets/Locksmith)
|
||||
[](http://cocoadocs.org/docsets/Locksmith)
|
||||
|
||||
## Installation
|
||||
|
||||
### CocoaPods
|
||||
|
||||
Locksmith is available through [CocoaPods](http://cocoapods.org). To install
|
||||
it, simply add the following line to your Podfile:
|
||||
Locksmith for Swift 2, simply add the following line to your Podfile:
|
||||
|
||||
pod "Locksmith"
|
||||
pod 'Locksmith'
|
||||
|
||||
> Swift 1.2 support is available via the `1.2.2` branch.
|
||||
|
||||
## Quick Start
|
||||
## Quick start
|
||||
|
||||
**Save Data**
|
||||
**Save data**
|
||||
|
||||
```swift
|
||||
Locksmith.saveData(["some key": "some value"], inService: "myService", forUserAccount: "myUserAccount")
|
||||
try Locksmith.saveData(["some key": "some value"], forUserAccount: "myUserAccount")
|
||||
```
|
||||
|
||||
**Load Data**
|
||||
**Load data**
|
||||
|
||||
```swift
|
||||
let (dictionary, error) = Locksmith.loadData(inService: "myService", forUserAccount: "myUserAccount")
|
||||
let dictionary = Locksmith.loadDataForUserAccount("myUserAccount")
|
||||
```
|
||||
|
||||
**Update Data**
|
||||
**Update data**
|
||||
|
||||
```swift
|
||||
Locksmith.updateData(["some key": "another value"], inService: "myService", forUserAccount: "myUserAccount")
|
||||
try Locksmith.updateData(["some key": "another value"], forUserAccount: "myUserAccount")
|
||||
```
|
||||
|
||||
**Delete Data**
|
||||
**Delete data**
|
||||
|
||||
```swift
|
||||
Locksmith.deleteData(inService: "myService", forUserAccount: "myUserAccount")
|
||||
try Locksmith.deleteDataForUserAccount("myUserAccount")
|
||||
```
|
||||
|
||||
## Custom Requests
|
||||
To create custom keychain requests, you first have to instantiate a `LocksmithRequest`. This request can be customised as much as required. Then call`Locksmith.performRequest` on that request.
|
||||
## Power to the protocols
|
||||
|
||||
Locksmith has been designed with Swift 2, protocols, and protocol extensions in mind.
|
||||
|
||||
Why do this? Because you can add existing functionality to your types with only the slightest changes!
|
||||
|
||||
Say we have a Twitter account
|
||||
|
||||
**Saving**
|
||||
```swift
|
||||
let saveRequest = LocksmithRequest(service: service, userAccount: userAccount, data: ["some key": "some value"])
|
||||
// Customize the request
|
||||
saveRequest.synchronizable = true
|
||||
Locksmith.performRequest(saveRequest)
|
||||
struct TwitterAccount {
|
||||
let username: String
|
||||
let password: String
|
||||
}
|
||||
```
|
||||
|
||||
**Reading**
|
||||
and we want to save it to the keychain as a generic password. All we need to do is conform to the right protocols in Locksmith and we get that functionality for free.
|
||||
|
||||
```swift
|
||||
let readRequest = LocksmithRequest(service: service, userAccount: userAccount)
|
||||
let (dictionary, error) = Locksmith.performRequest(readRequest)
|
||||
struct TwitterAccount: CreateableSecureStorable, GenericPasswordSecureStorable {
|
||||
let username: String
|
||||
let password: String
|
||||
|
||||
// Required by GenericPasswordSecureStorable
|
||||
let service = "Twitter"
|
||||
var account: String { return username }
|
||||
|
||||
// Required by CreateableSecureStorable
|
||||
var data: [String: AnyObject] {
|
||||
return ["password": password]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Deleting**
|
||||
Now we get the ability to save our account in the keychain.
|
||||
|
||||
```swift
|
||||
let deleteRequest = LocksmithRequest(service: service, userAccount: userAccount, requestType: .Delete)
|
||||
Locksmith.performRequest(deleteRequest)
|
||||
let account = TwitterAccount(username: "_matthewpalmer", password: "my_password")
|
||||
try account.createInSecureStore()
|
||||
```
|
||||
|
||||
## LocksmithRequest
|
||||
Use these attributes to customize your `LocksmithRequest` instance.
|
||||
Creating, reading, and deleting each have their own protocols: `CreateableSecureStorable`, `ReadableSecureStorable`, and `DeleteableSecureStorable`. And the best part?
|
||||
|
||||
If you need any more custom attributes, either create a pull request or open an issue.
|
||||
**You can conform to all three protocols on the same type!**
|
||||
|
||||
```swift
|
||||
struct TwitterAccount: ReadableSecureStorable,
|
||||
CreateableSecureStorable,
|
||||
DeleteableSecureStorable,
|
||||
GenericPasswordSecureStorable {
|
||||
let username: String
|
||||
let password: String
|
||||
|
||||
let service = "Twitter"
|
||||
var account: String { return username }
|
||||
var data: [String: AnyObject] {
|
||||
return ["password": password]
|
||||
}
|
||||
}
|
||||
|
||||
let account = TwitterAccount(username: "_matthewpalmer", password: "my_password")
|
||||
|
||||
// CreateableSecureStorable lets us create the account in the keychain
|
||||
try account.createInSecureStore()
|
||||
|
||||
// ReadableSecureStorable lets us read the account from the keychain
|
||||
let result = account.readFromSecureStore()
|
||||
|
||||
// DeleteableSecureStorable lets us delete the account from the keychain
|
||||
try account.deleteFromSecureStore()
|
||||
```
|
||||
|
||||
So. cool.
|
||||
|
||||
### The details
|
||||
|
||||
By declaring that your type adopts these protocols—which is what we did above with `struct TwitterAccount: CreateableSecureStorable, ...`—you get a bunch of functionality for free.
|
||||
|
||||
I like to think about protocols with extensions in terms of “what you get,” “what you’ve gotta do,” and ”what’s optional.” Most of the stuff under ‘optional’ should only be implemented if you want to change existing functionality.
|
||||
|
||||
#### `CreateableSecureStorable`
|
||||
|
||||
**What you get**
|
||||
|
||||
```swift
|
||||
// Saves a type to the keychain
|
||||
func createInSecureStore() throws
|
||||
```
|
||||
|
||||
**Required**
|
||||
|
||||
```swift
|
||||
var service: String
|
||||
var userAccount: String
|
||||
var type: RequestType // Defaults to .Read
|
||||
// The data to save to the keychain
|
||||
var data: [String: AnyObject] { get }
|
||||
```
|
||||
|
||||
**Optional**
|
||||
|
||||
```swift
|
||||
var group: String? // Used for keychain sharing
|
||||
var data: NSDictionary? // Used only for write requests
|
||||
var matchLimit: MatchLimit // Defaults to .One
|
||||
var securityClass: SecurityClass // Defaults to .GenericPassword
|
||||
var synchronizable: Bool // Defaults to false
|
||||
// Perform the request in this closure
|
||||
var performCreateRequestClosure: PerformRequestClosureType { get }
|
||||
```
|
||||
|
||||
#### `ReadableSecureStorable`
|
||||
|
||||
**What you get**
|
||||
|
||||
```swift
|
||||
// Read from the keychain
|
||||
func readFromSecureStore() -> SecureStorableResultType?
|
||||
```
|
||||
|
||||
**Required**
|
||||
|
||||
> Nothing!
|
||||
|
||||
**Optional**
|
||||
|
||||
```swift
|
||||
// Perform the request in this closure
|
||||
var performReadRequestClosure: PerformRequestClosureType { get }
|
||||
```
|
||||
|
||||
#### `DeleteableSecureStorable`
|
||||
|
||||
**What you get**
|
||||
|
||||
```swift
|
||||
// Read from the keychain
|
||||
func deleteFromSecureStore() throws
|
||||
```
|
||||
|
||||
**Required**
|
||||
|
||||
> Nothing!
|
||||
|
||||
**Optional**
|
||||
|
||||
```swift
|
||||
// Perform the request in this closure
|
||||
var performDeleteRequestClosure: PerformRequestClosureType { get }
|
||||
```
|
||||
|
||||
## Powerful support for the Cocoa Keychain
|
||||
|
||||
Many wrappers around the keychain have only support certain parts of the API. This is because there are so many options and variations on the way you can query the keychain that it’s almost impossible to abstract effectively.
|
||||
|
||||
Locksmith tries to include as much of the keychain as possible, using protocols and protocol extensions to minimize the complexity. You can mix-and-match your generic passwords with your read requests while staying completely type-safe.
|
||||
|
||||
Please refer to the [Keychain Services Reference](https://developer.apple.com/library/ios/documentation/Security/Reference/keychainservices/) for full information on what each of the attributes mean and what they can do.
|
||||
|
||||
> Certificates, keys, and identities are coming soon—it’s just a matter of translating the `kSec...` constants!
|
||||
|
||||
#### `GenericPasswordSecureStorable`
|
||||
|
||||
Generic passwords are probably the most common use-case of the keychain, and are great for storing usernames and passwords.
|
||||
|
||||
Properties listed under ‘Required’ have to be implemented by any types that conform; those listed under ‘Optional’ can be implemented to add additional information to what is saved or read if desired.
|
||||
|
||||
One thing to note: if you implement an optional property, its type annotation must match the type specified in the protocol *exactly*. If you implement `description: String?` it can’t be declared as `var description: String`.
|
||||
|
||||
**Required**
|
||||
|
||||
```swift
|
||||
var account: String { get }
|
||||
var service: String { get }
|
||||
```
|
||||
|
||||
**Optional**
|
||||
|
||||
```swift
|
||||
var comment: String? { get }
|
||||
var creator: UInt? { get }
|
||||
var description: String? { get }
|
||||
var generic: NSData? { get }
|
||||
var isInvisible: Bool? { get }
|
||||
var isNegative: Bool? { get }
|
||||
var label: String? { get }
|
||||
var type: UInt? { get }
|
||||
```
|
||||
|
||||
#### `InternetPasswordSecureStorable`
|
||||
|
||||
Types that conform to `InternetPasswordSecureStorable` typically come from web services and have certain associated metadata.
|
||||
|
||||
**Required**
|
||||
|
||||
```swift
|
||||
var account: String { get }
|
||||
var authenticationType: LocksmithInternetAuthenticationType { get }
|
||||
var internetProtocol: LocksmithInternetProtocol { get }
|
||||
var port: String { get }
|
||||
var server: String { get }
|
||||
```
|
||||
|
||||
**Optional**
|
||||
|
||||
```swift
|
||||
var comment: String? { get }
|
||||
var creator: UInt? { get }
|
||||
var description: String? { get }
|
||||
var isInvisible: Bool? { get }
|
||||
var isNegative: Bool? { get }
|
||||
var path: String? { get }
|
||||
var securityDomain: String? { get }
|
||||
var type: UInt? { get }
|
||||
```
|
||||
|
||||
## Result types
|
||||
|
||||
By adopting a protocol-oriented design from the ground up, Locksmith can provide access to the result of your keychain queries *with type annotations included*—store an `NSDate`, get an `NSDate` back with no type-casting!
|
||||
|
||||
Let’s start with an example: the Twitter account from before, except it’s now an `InternetPasswordSecureStorable`, which lets us store a bit more metadata.
|
||||
|
||||
```swift
|
||||
struct TwitterAccount: InternetPasswordSecureStorable,
|
||||
ReadableSecureStorable,
|
||||
CreateableSecureStorable {
|
||||
let username: String
|
||||
let password: String
|
||||
|
||||
var account: String { return username }
|
||||
var data: [String: AnyObject] {
|
||||
return ["password": password]
|
||||
}
|
||||
|
||||
let server = "com.twitter"
|
||||
let port = 80
|
||||
let internetProtocol = .HTTPS
|
||||
let authenticationType = .HTTPBasic
|
||||
let path: String? = "/api/2.0/"
|
||||
}
|
||||
|
||||
let account = TwitterAccount(username: "_matthewpalmer", password: "my_password")
|
||||
|
||||
// Save all this to the keychain
|
||||
account.createInSecureStore()
|
||||
|
||||
// Now let’s get it back
|
||||
let result: InternetPasswordSecureStorableResultType = account.readFromSecureStore()
|
||||
|
||||
result?.port // Gives us an Int directly!
|
||||
result?.internetProtocol // Gives us a LocksmithInternetProtocol enum case directly!
|
||||
result?.data // Gives us a [String: AnyObject] of what was saved
|
||||
// and so on...
|
||||
```
|
||||
|
||||
This is *awesome*. No more typecasting.
|
||||
|
||||
#### `GenericPasswordSecureStorableResultType`
|
||||
|
||||
Everything listed here can be set on a type conforming to `GenericPasswordSecureStorable`, and gotten back from the result returned from `readFromSecureStore()` on that type.
|
||||
|
||||
```swift
|
||||
var account: String { get }
|
||||
var service: String { get }
|
||||
var comment: String? { get }
|
||||
var creator: UInt? { get }
|
||||
var description: String? { get }
|
||||
var data: [String: AnyObject]? { get }
|
||||
var generic: NSData? { get }
|
||||
var isInvisible: Bool? { get }
|
||||
var isNegative: Bool? { get }
|
||||
var label: String? { get }
|
||||
var type: UInt? { get }
|
||||
```
|
||||
|
||||
#### `InternetPasswordSecureStorableResultType`
|
||||
|
||||
Everything listed here can be set on a type conforming to `InternetPasswordSecureStorable`, and gotten back from the result returned from `readFromSecureStore()` on that type.
|
||||
|
||||
```swift
|
||||
var account: String { get }
|
||||
var authenticationType: LocksmithInternetAuthenticationType { get }
|
||||
var internetProtocol: LocksmithInternetProtocol { get }
|
||||
var port: Int { get }
|
||||
var server: String { get }
|
||||
var comment: String? { get }
|
||||
var creator: UInt? { get }
|
||||
var data: [String: AnyObject]? { get }
|
||||
var description: String? { get }
|
||||
var isInvisible: Bool? { get }
|
||||
var isNegative: Bool? { get }
|
||||
var path: String? { get }
|
||||
var securityDomain: String? { get }
|
||||
var type: UInt? { get }
|
||||
```
|
||||
|
||||
## Enumerations
|
||||
|
||||
Locksmith provides a bunch of handy enums for configuring your requests, so you can say `kSecGoodByeStringConstants`.
|
||||
|
||||
#### `LocksmithAccessibleOption`
|
||||
|
||||
`LocksmithAccessibleOption` configures when an item can be accessed—you might require that stuff is available when the device is unlocked, after a passcode has been entered, etc.
|
||||
|
||||
```swift
|
||||
public enum LocksmithAccessibleOption {
|
||||
case AfterFirstUnlock
|
||||
case AfterFirstUnlockThisDeviceOnly
|
||||
case Always
|
||||
case AlwaysThisDeviceOnly
|
||||
case WhenPasscodeSetThisDeviceOnly
|
||||
case WhenUnlocked
|
||||
case WhenUnlockedThisDeviceOnly
|
||||
}
|
||||
```
|
||||
|
||||
#### `LocksmithError`
|
||||
|
||||
`LocksmithError` provides Swift-friendly translations of common keychain error codes. These are thrown from methods throughout the library.
|
||||
|
||||
```swift
|
||||
public enum LocksmithError: ErrorType {
|
||||
case Allocate
|
||||
case AuthFailed
|
||||
case Decode
|
||||
case Duplicate
|
||||
case InteractionNotAllowed
|
||||
case NoError
|
||||
case NotAvailable
|
||||
case NotFound
|
||||
case Param
|
||||
case RequestNotSet
|
||||
case TypeNotFound
|
||||
case UnableToClear
|
||||
case Undefined
|
||||
case Unimplemented
|
||||
}
|
||||
```
|
||||
|
||||
#### `LocksmithInternetAuthenticationType`
|
||||
|
||||
`LocksmithInternetAuthenticationType` lets you pick out the type of authentication you want to store alongside your `.InternetPassword`s—anything from `.MSN` to `.HTTPDigest`.
|
||||
|
||||
```swift
|
||||
public enum LocksmithInternetAuthenticationType {
|
||||
case Default
|
||||
case DPA
|
||||
case HTMLForm
|
||||
case HTTPBasic
|
||||
case HTTPDigest
|
||||
case MSN
|
||||
case NTLM
|
||||
case RPA
|
||||
}
|
||||
```
|
||||
|
||||
#### `LocksmithInternetProtocol`
|
||||
|
||||
`LocksmithInternetProtocol` is used with `.InternetPassword` to choose which protocol was used for the interaction with the web service, including `.HTTP`, `.SMB`, and a whole bunch more.
|
||||
|
||||
public enum {
|
||||
case AFP
|
||||
case AppleTalk
|
||||
case DAAP
|
||||
case EPPC
|
||||
case FTP
|
||||
case FTPAccount
|
||||
case FTPProxy
|
||||
case FTPS
|
||||
case HTTP
|
||||
case HTTPProxy
|
||||
case HTTPS
|
||||
case HTTPSProxy
|
||||
case IMAP
|
||||
case IMAPS
|
||||
case IPP
|
||||
case IRC
|
||||
case IRCS
|
||||
case LDAP
|
||||
case NNTP
|
||||
case NNTPS, LDAPS
|
||||
case POP3
|
||||
case POP3S
|
||||
case RTSP
|
||||
case RTSPProxy
|
||||
case SMB
|
||||
case SMTP
|
||||
case SOCKS
|
||||
case SSH
|
||||
case Telnet
|
||||
case TelnetS
|
||||
}
|
||||
```
|
||||
|
||||
## Author
|
||||
|
||||
Reference in New Issue
Block a user