Compare commits

...

29 Commits

Author SHA1 Message Date
matthewpalmer 41b6644b18 Set version to 2.0.0 2015-08-31 21:38:49 +10:00
matthewpalmer a449ebf527 Update compatibility branch name 2015-08-31 21:29:59 +10:00
matthewpalmer ccda8fb901 Update README 2015-08-31 21:02:40 +10:00
matthewpalmer f94eff2115 Update README 2015-08-31 20:47:32 +10:00
matthewpalmer ee96b18cfd Add tests to clarify closure bug 2015-08-31 19:42:32 +10:00
matthewpalmer 79cf22216f Refactor into separate files and fix closure bug 2015-08-31 19:42:16 +10:00
matthewpalmer 4bfbf86f73 Add the ability to conform to all 3 protocols 2015-08-31 18:58:01 +10:00
matthewpalmer f0c4572ff4 Add test for deleting internet password 2015-08-31 18:27:11 +10:00
matthewpalmer af441ab648 Add internet password request metadata support 2015-08-30 22:23:24 +10:00
matthewpalmer 6074c2640a Add ability to retrieve result metadata from request 2015-08-30 20:31:58 +10:00
matthewpalmer 351b0a4c82 Update readme with code fix 2015-08-30 15:23:09 +10:00
matthewpalmer 38b29df3ca Update installation steps 2015-08-30 14:22:15 +10:00
matthewpalmer 9efa16d266 Remove br 2015-08-30 14:15:26 +10:00
matthewpalmer 709d323463 Fix README typos 2015-08-30 14:14:46 +10:00
matthewpalmer e06271dd90 Major rewrite to be protocol-oriented 2015-08-30 14:06:48 +10:00
matthewpalmer 1b40db9405 Add loading all data for a service.
* Note: this only works for .GenericPasswords. I think I've found
  a bug in trying to load other types of security classes,
  so will need to investigate that further
2015-08-23 19:39:00 +10:00
matthewpalmer afe4706789 Manually merge change to [String: AnyObject] 2015-08-22 22:21:37 +10:00
matthewpalmer a89f2584ce Merge branch 'kayvink-master' into swift-2.0-further-changes
* kayvink-master:
2015-08-22 22:14:22 +10:00
matthewpalmer cb920c0318 Merge branch 'master' of https://github.com/kayvink/Locksmith into kayvink-master
* 'master' of https://github.com/kayvink/Locksmith:
  - Changed saveData from Dictionary<String,String> to Dictionary<String,AnyObjec> - Changed updateData from Dictionary<String,String> to Dictionary<String,AnyObjec>
  - Changed NSDictionary to Dictionary<String:AnyObject> - Added support for other file types beside String
  Update README.md
2015-08-22 22:10:45 +10:00
matthewpalmer b11dea36b4 Merge branch '2.0' into getaaron-keychain-enumeration
* 2.0:
  Update version number
  Update README to reflect Swift 2 changes
  Change loadData method to be non-throwing
  Update tests for new error handling mechanisms
  Refactor for Swift 2
  Major update for Swift 2
  Add tests for Swift 2 style errors
  Update README.md
  + Updates to the new Swift 2 syntax
2015-08-22 22:04:05 +10:00
matthewpalmer 07a9234d3e Merge branch 'master' of https://github.com/kayvink/Locksmith into kayvink-master
* 'master' of https://github.com/kayvink/Locksmith:
  - Changed saveData from Dictionary<String,String> to Dictionary<String,AnyObjec> - Changed updateData from Dictionary<String,String> to Dictionary<String,AnyObjec>
  - Changed NSDictionary to Dictionary<String:AnyObject> - Added support for other file types beside String
2015-08-22 21:22:16 +10:00
matthewpalmer b193e300a5 Add fixes from #38 2015-08-22 21:05:06 +10:00
matthewpalmer 7e53fc8322 Merge branch 'keychain-enumeration' of https://github.com/getaaron/Locksmith into getaaron-keychain-enumeration
* 'keychain-enumeration' of https://github.com/getaaron/Locksmith:
  Make user account optional
  Deprecate .Many and replace it with .All
  Initial implementation of each
  Update README.md
2015-08-22 20:48:13 +10:00
Kay Vink 9ce641f262 - Changed saveData from Dictionary<String,String> to Dictionary<String,AnyObjec>
- Changed updateData from Dictionary<String,String> to Dictionary<String,AnyObjec>

- Changed LocksmithRequest data from NSDictionary? to Dictionary<String,Anyobject>?
- Updated init accordingly
2015-08-21 16:13:45 +02:00
Kay Vink 17d52574ab - Changed NSDictionary to Dictionary<String:AnyObject>
- Added support for other file types beside String
2015-08-21 15:53:57 +02:00
Aaron Brager 5602015a6d Make user account optional 2015-08-12 12:39:29 +02:00
Aaron Brager 737c262e8b Deprecate .Many and replace it with .All 2015-08-12 12:02:40 +02:00
Aaron Brager 8dc3ef57c6 Initial implementation of each 2015-08-12 12:02:03 +02:00
Matthew Palmer 0dda3a7208 Update README.md
Add note on Swift 2 support
2015-06-23 07:42:03 +10:00
14 changed files with 1720 additions and 505 deletions
@@ -206,11 +206,6 @@
LastSwiftUpdateCheck = 0700;
LastUpgradeCheck = 0510;
ORGANIZATIONNAME = matthewpalmer;
TargetAttributes = {
6003F5AD195388D20070C39A = {
TestTargetID = 6003F589195388D20070C39A;
};
};
};
buildConfigurationList = 6003F585195388D10070C39A /* Build configuration list for PBXProject "Locksmith" */;
compatibilityVersion = "Xcode 3.2";
@@ -486,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",
@@ -504,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;
@@ -513,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",
@@ -526,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;
+40 -8
View File
@@ -13,6 +13,19 @@
07A4B71C50D9597CF917D4FE /* EXPMatchers.h in Headers */ = {isa = PBXBuildFile; fileRef = 65EFE3DF93A99DBAC7EFBEC2 /* EXPMatchers.h */; settings = {ATTRIBUTES = (Public, ); }; };
0B5CF8891029C5DFBAD3FA3C /* SPTExample.h in Headers */ = {isa = PBXBuildFile; fileRef = D48C5C55D0644D7CB0CD3551 /* SPTExample.h */; settings = {ATTRIBUTES = (Public, ); }; };
0E4571BBF518AE8F42E817B3 /* EXPMatchers+endWith.h in Headers */ = {isa = PBXBuildFile; fileRef = 93B6CDD1C13BE6F21D7C0D6E /* EXPMatchers+endWith.h */; settings = {ATTRIBUTES = (Public, ); }; };
0E460E811B92B7D000D5241B /* Locksmith.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF96FAE7F7FF350B0CB2B3C7 /* Locksmith.swift */; };
0E460E831B944F4300D5241B /* LocksmithInternetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E460E821B944F4300D5241B /* LocksmithInternetProtocol.swift */; };
0E460E841B944F4300D5241B /* LocksmithInternetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E460E821B944F4300D5241B /* LocksmithInternetProtocol.swift */; };
0E460E861B944FB800D5241B /* LocksmithError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E460E851B944FB800D5241B /* LocksmithError.swift */; };
0E460E871B944FB800D5241B /* LocksmithError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E460E851B944FB800D5241B /* LocksmithError.swift */; };
0E460E891B944FF800D5241B /* LocksmithSecurityClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E460E881B944FF800D5241B /* LocksmithSecurityClass.swift */; };
0E460E8A1B944FF800D5241B /* LocksmithSecurityClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E460E881B944FF800D5241B /* LocksmithSecurityClass.swift */; };
0E460E8C1B94502500D5241B /* LocksmithInternetAuthenticationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E460E8B1B94502500D5241B /* LocksmithInternetAuthenticationType.swift */; };
0E460E8D1B94502500D5241B /* LocksmithInternetAuthenticationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E460E8B1B94502500D5241B /* LocksmithInternetAuthenticationType.swift */; };
0E460E8F1B94504B00D5241B /* LocksmithAccessibleOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E460E8E1B94504B00D5241B /* LocksmithAccessibleOption.swift */; };
0E460E901B94504B00D5241B /* LocksmithAccessibleOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E460E8E1B94504B00D5241B /* LocksmithAccessibleOption.swift */; };
0E460E921B9451E900D5241B /* Dictionary_Initializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E460E911B9451E900D5241B /* Dictionary_Initializers.swift */; };
0E460E931B9451E900D5241B /* Dictionary_Initializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E460E911B9451E900D5241B /* Dictionary_Initializers.swift */; };
10C30956E8ADE7A1C87DA2EB /* SpectaSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 00234EE34EBD5D8B8A2CC35A /* SpectaSupport.h */; settings = {ATTRIBUTES = (Public, ); }; };
14168A1F75A19980EFD2D2E5 /* EXPMatchers+equal.h in Headers */ = {isa = PBXBuildFile; fileRef = EDBE72B542ED9074425509EB /* EXPMatchers+equal.h */; settings = {ATTRIBUTES = (Public, ); }; };
19AB0D01E5490DB99785D465 /* EXPMatchers+raise.h in Headers */ = {isa = PBXBuildFile; fileRef = 3086223A59C78DD82DADBF33 /* EXPMatchers+raise.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -56,7 +69,6 @@
5E2FD81F9B213781D134CF33 /* XCTestRun+Specta.h in Headers */ = {isa = PBXBuildFile; fileRef = CE3402F17C786AA866ED4184 /* XCTestRun+Specta.h */; settings = {ATTRIBUTES = (Public, ); }; };
60871D3B9E631A69D701881D /* EXPMatchers+notify.m in Sources */ = {isa = PBXBuildFile; fileRef = 3664AF859D0947E85BD8ADC5 /* EXPMatchers+notify.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
60E3837FC9535B3DB6A55A39 /* EXPMatchers+beSupersetOf.h in Headers */ = {isa = PBXBuildFile; fileRef = C9BE9072FDBDE322B2EA2CF8 /* EXPMatchers+beSupersetOf.h */; settings = {ATTRIBUTES = (Public, ); }; };
6178C30ADF1231DDC3671C9B /* LocksmithRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF2E53D799048701CD641B75 /* LocksmithRequest.swift */; };
64FEA2541907D8B2727AA88D /* EXPMatchers+beInstanceOf.m in Sources */ = {isa = PBXBuildFile; fileRef = DADA5F60ABEAB0D22A8FEA6B /* EXPMatchers+beInstanceOf.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
6563354F35A4B7764F0AAAB2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9E6D6D61DC0B85CFDC896E1E /* Foundation.framework */; };
68BF8DDB6233161110E31AB0 /* EXPBlockDefinedMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = CE9CAFBC6CE6CC0D7919AE88 /* EXPBlockDefinedMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -69,7 +81,6 @@
721930BAF47FD79EA044A058 /* EXPMatchers+respondTo.m in Sources */ = {isa = PBXBuildFile; fileRef = DA59E72B0EBCE7546D636796 /* EXPMatchers+respondTo.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
7328DA8830FE9F1529D31993 /* SPTXCTestReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = EBF978B58927361F7366B880 /* SPTXCTestReporter.h */; settings = {ATTRIBUTES = (Public, ); }; };
737A765D45D8C6F58236756D /* EXPMatchers+respondTo.h in Headers */ = {isa = PBXBuildFile; fileRef = 88F119C17D8261E851EB20D5 /* EXPMatchers+respondTo.h */; settings = {ATTRIBUTES = (Public, ); }; };
796912D454A5B93D65D6F91E /* Locksmith.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF96FAE7F7FF350B0CB2B3C7 /* Locksmith.swift */; };
7B9537FD66766934E5558676 /* EXPDoubleTuple.m in Sources */ = {isa = PBXBuildFile; fileRef = 9CA84F8CF432584BBC70748B /* EXPDoubleTuple.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
7E38C020DB81BBF88D9222B8 /* XCTestLog+Specta.m in Sources */ = {isa = PBXBuildFile; fileRef = C1A44F6F4215603D9E89E41A /* XCTestLog+Specta.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; };
7E815AE22AFE1C28581211F6 /* ExpectaSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 473488C99695CA079D32EDBD /* ExpectaSupport.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -84,7 +95,6 @@
8C1B8746364E4D9E3B893748 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9E6D6D61DC0B85CFDC896E1E /* Foundation.framework */; };
8E708CCDCC73888523C30107 /* Pods-Tests-Locksmith-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 7D038C4A26BDE4ED9F64DA9F /* Pods-Tests-Locksmith-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
8FF363667C3364E6AAEE9A77 /* EXPDoubleTuple.h in Headers */ = {isa = PBXBuildFile; fileRef = 0C4004DC631978F7FC5DF072 /* EXPDoubleTuple.h */; settings = {ATTRIBUTES = (Public, ); }; };
922638F637E8B15970504607 /* LocksmithRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF2E53D799048701CD641B75 /* LocksmithRequest.swift */; };
9373C9B0EE80CD4B79BB5FFF /* Pods-Tests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 7395FFDBE01E3C1A49DD3501 /* Pods-Tests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
96CBFDF35FAAADF325BA110D /* EXPMatchers+conformTo.h in Headers */ = {isa = PBXBuildFile; fileRef = 1DFDCFE7C93F09A152EE1A3B /* EXPMatchers+conformTo.h */; settings = {ATTRIBUTES = (Public, ); }; };
9D5A95A1D2BD0C3E8C0BA213 /* EXPMatchers+beCloseTo.m in Sources */ = {isa = PBXBuildFile; fileRef = 5667AE19B7E489A5C53FBB92 /* EXPMatchers+beCloseTo.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
@@ -191,6 +201,12 @@
0C4004DC631978F7FC5DF072 /* EXPDoubleTuple.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = EXPDoubleTuple.h; path = src/EXPDoubleTuple.h; sourceTree = "<group>"; };
0DFA3BB43F6A3B0157A90070 /* EXPMatchers+beGreaterThanOrEqualTo.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "EXPMatchers+beGreaterThanOrEqualTo.m"; path = "src/matchers/EXPMatchers+beGreaterThanOrEqualTo.m"; sourceTree = "<group>"; };
0E198AA4AC832494E0B63E5E /* Pods-Locksmith-Locksmith-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-Locksmith-Locksmith-dummy.m"; sourceTree = "<group>"; };
0E460E821B944F4300D5241B /* LocksmithInternetProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocksmithInternetProtocol.swift; sourceTree = "<group>"; };
0E460E851B944FB800D5241B /* LocksmithError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocksmithError.swift; sourceTree = "<group>"; };
0E460E881B944FF800D5241B /* LocksmithSecurityClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocksmithSecurityClass.swift; sourceTree = "<group>"; };
0E460E8B1B94502500D5241B /* LocksmithInternetAuthenticationType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocksmithInternetAuthenticationType.swift; sourceTree = "<group>"; };
0E460E8E1B94504B00D5241B /* LocksmithAccessibleOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocksmithAccessibleOption.swift; sourceTree = "<group>"; };
0E460E911B9451E900D5241B /* Dictionary_Initializers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dictionary_Initializers.swift; sourceTree = "<group>"; };
12A029F492765C4C99EB830C /* XCTestCase+Specta.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "XCTestCase+Specta.m"; path = "src/XCTestCase+Specta.m"; sourceTree = "<group>"; };
12C3719A9EB3BD854C6E87A1 /* EXPFloatTuple.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = EXPFloatTuple.h; path = src/EXPFloatTuple.h; sourceTree = "<group>"; };
1549D4BE3B7613E6F6BE4B04 /* EXPMatchers+haveCountOf.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "EXPMatchers+haveCountOf.m"; path = "src/matchers/EXPMatchers+haveCountOf.m"; sourceTree = "<group>"; };
@@ -336,7 +352,6 @@
EBF978B58927361F7366B880 /* SPTXCTestReporter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SPTXCTestReporter.h; path = src/SPTXCTestReporter.h; sourceTree = "<group>"; };
EDBE72B542ED9074425509EB /* EXPMatchers+equal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "EXPMatchers+equal.h"; path = "src/matchers/EXPMatchers+equal.h"; sourceTree = "<group>"; };
EEE8816D049FD510B554FFDA /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
EF2E53D799048701CD641B75 /* LocksmithRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocksmithRequest.swift; sourceTree = "<group>"; };
EF3657385FF29C09AA38DC9D /* Pods-Locksmith-Locksmith.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = "sourcecode.module-map"; path = "Pods-Locksmith-Locksmith.modulemap"; sourceTree = "<group>"; };
EFD37A935FC49CABEAC95838 /* Pods-Locksmith-Locksmith.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Locksmith-Locksmith.xcconfig"; sourceTree = "<group>"; };
F0C5BBB4A14F0B660AFAC239 /* EXPMatchers+beInTheRangeOf.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "EXPMatchers+beInTheRangeOf.m"; path = "src/matchers/EXPMatchers+beInTheRangeOf.m"; sourceTree = "<group>"; };
@@ -580,7 +595,12 @@
isa = PBXGroup;
children = (
DF96FAE7F7FF350B0CB2B3C7 /* Locksmith.swift */,
EF2E53D799048701CD641B75 /* LocksmithRequest.swift */,
0E460E911B9451E900D5241B /* Dictionary_Initializers.swift */,
0E460E8E1B94504B00D5241B /* LocksmithAccessibleOption.swift */,
0E460E8B1B94502500D5241B /* LocksmithInternetAuthenticationType.swift */,
0E460E881B944FF800D5241B /* LocksmithSecurityClass.swift */,
0E460E851B944FB800D5241B /* LocksmithError.swift */,
0E460E821B944F4300D5241B /* LocksmithInternetProtocol.swift */,
);
path = Classes;
sourceTree = "<group>";
@@ -1078,7 +1098,14 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0E460E811B92B7D000D5241B /* Locksmith.swift in Sources */,
0E460E8C1B94502500D5241B /* LocksmithInternetAuthenticationType.swift in Sources */,
2053EC95BD153CAF516EA7EE /* Pods-Locksmith-dummy.m in Sources */,
0E460E8F1B94504B00D5241B /* LocksmithAccessibleOption.swift in Sources */,
0E460E921B9451E900D5241B /* Dictionary_Initializers.swift in Sources */,
0E460E831B944F4300D5241B /* LocksmithInternetProtocol.swift in Sources */,
0E460E891B944FF800D5241B /* LocksmithSecurityClass.swift in Sources */,
0E460E861B944FB800D5241B /* LocksmithError.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1087,8 +1114,13 @@
buildActionMask = 2147483647;
files = (
D43013AD8CD8A7F330BE2BC4 /* Locksmith.swift in Sources */,
922638F637E8B15970504607 /* LocksmithRequest.swift in Sources */,
0E460E8D1B94502500D5241B /* LocksmithInternetAuthenticationType.swift in Sources */,
AEFCE8AD57F178A2FA9339BF /* Pods-Tests-Locksmith-dummy.m in Sources */,
0E460E901B94504B00D5241B /* LocksmithAccessibleOption.swift in Sources */,
0E460E931B9451E900D5241B /* Dictionary_Initializers.swift in Sources */,
0E460E841B944F4300D5241B /* LocksmithInternetProtocol.swift in Sources */,
0E460E8A1B944FF800D5241B /* LocksmithSecurityClass.swift in Sources */,
0E460E871B944FB800D5241B /* LocksmithError.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1103,8 +1135,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
796912D454A5B93D65D6F91E /* Locksmith.swift in Sources */,
6178C30ADF1231DDC3671C9B /* LocksmithRequest.swift in Sources */,
6B44D8A6FE8B116C35B7D573 /* Pods-Locksmith-Locksmith-dummy.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1391,6 +1421,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9D17689735EB0279DFA27E08 /* Pods-Locksmith.release.xcconfig */;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
@@ -1486,6 +1517,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 314BC53FBA90AFDDB7704685 /* Pods-Locksmith.debug.xcconfig */;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
+387 -84
View File
@@ -7,106 +7,409 @@
//
import XCTest
import Locksmith
class LocksmithDemoTests: XCTestCase {
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.
try! 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() {
try! Locksmith.saveData(["key": "value"], forUserAccount: "myUserAccount", inService: "myService")
// XCTAssert(error == nil, ": saving data")
}
func testSaveData_Multiple() {
// var errors: [NSError?] = []
for i in 0...10 {
try! Locksmith.saveData(["key": "value \(i)"], forUserAccount: "myAccount\(i)", inService: "myService")
}
// XCTAssert(errors.filter({ $0 != nil }).isEmpty, ": saving multiple items")
}
func testSaveData_Duplicate() {
// Should be successful
try! Locksmith.saveData(["key": "value"], forUserAccount: "user", inService: "myService")
func testStaticMethods() {
let data = ["some": "data"]
try! Locksmith.saveData(data, forUserAccount: userAccount, inService: service)
// Should fail
do {
try Locksmith.saveData(["key": "value"], forUserAccount: "user", inService: "myService")
} catch {
XCTAssert(true)
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 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
}
let create = CreateGenericPassword(data: data, account: userAccount, service: service)
try! create.createInSecureStore() // make sure it doesn't throw
}
func testCreateForGenericPassword() {
let data = ["some": "data"]
createGenericPasswordWithData(data)
}
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)
}
func testDeleteForGenericPassword() {
let initialData = ["one": "two"]
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 testForConformanceToAll3Protocols() {
struct Omnivore: ReadableSecureStorable, CreateableSecureStorable, DeleteableSecureStorable, GenericPasswordSecureStorable {
let account: String
let service: String
let data: [String: AnyObject]
}
let data: [String: String] = ["something": "else"]
let omni = Omnivore(account: userAccount, service: service, data: data)
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)
}
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 server = "server"
let initialData = ["one": "two"]
let port = 8080
let internetProtocol = LocksmithInternetProtocol.HTTPS
let authenticationType = LocksmithInternetAuthenticationType.DPA
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)
}
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 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()
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 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)
}
}
// Setup the keychain for requests that use pre-existing values on the keychain (update, read, delete)
func setupLoads() {
try! Locksmith.saveData(["key": "value"], forUserAccount: "user1", inService: "myService")
try! Locksmith.saveData(["anotherkey": "anothervalue"], forUserAccount: "user2", inService: "myService")
try! Locksmith.saveData(["word": "definition"], forUserAccount: "user3", inService: "myService")
}
// public class func loadDataInService(service: String, forUserAccount userAccount: String) -> (NSDictionary?, NSError?)
func testLoadData_Once() {
setupLoads()
let dictionary = Locksmith.loadDataForUserAccount("user1", inService: "myService")
XCTAssert(dictionary!.valueForKey("key") as! NSString == "value", "❌: loading one item")
}
func testLoadData_Multiple() {
setupLoads()
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
}
do {
let dictionary = Locksmith.loadDataForUserAccount("user1", inService: "myService")
let dictionary2 = Locksmith.loadDataForUserAccount("user2", inService: "myService")
let dictionary3 = Locksmith.loadDataForUserAccount("user3", inService: "myService")
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
XCTAssert(dictionary!.valueForKey("key") as! NSString == "value", "❌: loading multiple items")
XCTAssert(dictionary2!.valueForKey("anotherkey") as! NSString == "anothervalue", "❌: loading multiple items")
XCTAssert(dictionary3!.valueForKey("word") as! NSString == "definition", "❌: loading multiple items")
} catch {
XCTAssert(false)
}
}
// public class func updateData(data: Dictionary<String, String>, inService service: String, forUserAccount userAccount: String) -> NSError?
func testUpdateData() {
setupLoads()
try! Locksmith.updateData(["key": "newvalue"], forUserAccount: "user1", inService: "myService")
let dictionary = Locksmith.loadDataForUserAccount("user1", inService: "myService")
XCTAssert(dictionary!.valueForKey("key") as! NSString == "newvalue", "❌: updating item")
// Updating an item that doesn't exist should create that item (i.e. performs a regular create request)
try! Locksmith.updateData(["key": "anothervalue"], forUserAccount: "user1", inService: "myService")
XCTAssert(true, "❌: updating item that doesn't exist")
}
// public class func deleteDataInService(service: String, forUserAccount userAccount: String) -> NSError?
func testDeleteData() {
setupLoads()
try! Locksmith.deleteDataForUserAccount("user1", inService: "myService")
XCTAssert(true, "❌: deleting existing item")
do {
// Should fail
try Locksmith.deleteDataForUserAccount("user1", inService: "myService")
XCTAssert(false, "❌: did not throw error on deleting non existent item")
} catch {
XCTAssert(true, "❌: deleting non existent item")
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
View File
@@ -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 = "2.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
+31
View File
@@ -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
}
}
}
+555 -199
View File
@@ -1,242 +1,598 @@
//
// Locksmith.swift
//
// Created by Matthew Palmer on 26/10/2014.
// Copyright (c) 2014 Colour Coding. All rights reserved.
//
import CoreFoundation
import UIKit
import Security
public let LocksmithDefaultService = NSBundle.mainBundle().infoDictionary![String(kCFBundleIdentifierKey)] as? String ?? "com.locksmith.defaultService"
// 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
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: Locksmith
public class Locksmith: NSObject {
// MARK: Perform request
public class func performRequest(request: LocksmithRequest) throws -> NSDictionary? {
let type = request.type
// 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
let parsedRequest: NSMutableDictionary = parseRequest(request)
let status = closure(requestReference: requestReference, result: &result)
let 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)
}
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
}
// hmmmm... bit leaky
if status != errSecSuccess {
return nil
}
return resultsDictionary
guard let dictionary = result as? NSDictionary else {
return nil
}
if dictionary[String(kSecValueData)] as? NSData == nil {
return nil
}
return result as? [String: AnyObject]
}
// 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)
}
public extension SecureStorable where Self : InternetPasswordSecureStorable {
private var internetPasswordBaseStoragePropertyDictionary: [String: AnyObject] {
var dictionary = [String: AnyObject]()
// 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()
// 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
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
let toMergeWith = [
self.accountSecureStoragePropertyDictionary,
self.describableSecureStoragePropertyDictionary,
self.commentableSecureStoragePropertyDictionary,
self.creatorDesignatableSecureStoragePropertyDictionary,
self.typeDesignatableSecureStoragePropertyDictionary,
self.isInvisibleSecureStoragePropertyDictionary,
self.isNegativeSecureStoragePropertyDictionary
]
if let accessibleMode = request.accessible {
options[String(kSecAttrAccessible)] = accessibleMode.rawValue
for dict in toMergeWith {
dictionary = Dictionary(initial: dictionary, toMerge: dict)
}
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 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 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
}
public class func loadDataForUserAccount(userAccount: String, inService service: String = LocksmithDefaultService) -> NSDictionary? {
let readRequest = LocksmithRequest(userAccount: userAccount, service: service)
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?]()
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(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
}
var server: String {
return stringFromResultDictionary(kSecAttrServer)!
}
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)) }
}
}
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 dictionary = try Locksmith.performRequest(readRequest)
return dictionary
let result = try performSecureStorageAction(performReadRequestClosure, secureStoragePropertyDictionary: self.asReadableSecureStoragePropertyDictionary)
return GenericPasswordResult(resultDictionary: result!)
} catch {
print(error)
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
}
}
}
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
}
}
}
// MARK: Dictionary Extension
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)
}
}
}
+44
View File
@@ -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)
}
}
}
+141
View File
@@ -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)
}
}
}
-142
View File
@@ -1,142 +0,0 @@
//
// LocksmithRequest.swift
//
// Created by Matthew Palmer on 26/10/2014.
// Copyright (c) 2014 Colour Coding. All rights reserved.
//
import UIKit
import Security
// With thanks to http://iosdeveloperzone.com/2014/10/22/taming-foundation-constants-into-swift-enums/
// MARK: Security Class
public enum SecurityClass: 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)
}
}
}
// MARK: Accessible
public enum Accessible: 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)
}
}
}
// MARK: Match Limit
public enum MatchLimit {
case One, Many
}
// MARK: Request Type
public enum RequestType {
case Create, Read, Update, Delete
}
// MARK: Locksmith Request
public class LocksmithRequest: NSObject, CustomDebugStringConvertible {
// Keychain Options
// Required
public var service: String = LocksmithDefaultService
public var userAccount: String
public var type: RequestType = .Read // Default to non-destructive
// Optional
public var securityClass: SecurityClass = .GenericPassword // Default to password lookup
public var group: String?
public var data: NSDictionary?
public var matchLimit: MatchLimit = .One
public var synchronizable = false
public var accessible: Accessible?
// Debugging
override public var debugDescription: String {
return "service: \(self.service), type: \(self.type), userAccount: \(self.userAccount)"
}
required public init(userAccount: String, service: String = LocksmithDefaultService) {
self.service = service
self.userAccount = userAccount
}
public convenience init(userAccount: String, requestType: RequestType, service: String = LocksmithDefaultService) {
self.init(userAccount: userAccount, service: service)
self.type = requestType
}
public convenience init(userAccount: String, requestType: RequestType, data: NSDictionary, service: String = LocksmithDefaultService) {
self.init(userAccount: userAccount, requestType: requestType, service: service)
self.data = data
}
}
+40
View File
@@ -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)
}
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.1.2</string>
<string>1.1.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
+381 -44
View File
@@ -1,14 +1,19 @@
# 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?**
* Locksmiths API is both super-simple and deeply powerful
* Provides access to all of the keychains 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
<!--[![CI Status](http://img.shields.io/travis/matthewpalmer/Locksmith.svg?style=flat)](https://travis-ci.org/matthewpalmer/Locksmith)-->
[![Version](https://img.shields.io/cocoapods/v/Locksmith.svg?style=flat)](http://cocoadocs.org/docsets/Locksmith)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![License](https://img.shields.io/cocoapods/l/Locksmith.svg?style=flat)](http://cocoadocs.org/docsets/Locksmith)
[![Platform](https://img.shields.io/cocoapods/p/Locksmith.svg?style=flat)](http://cocoadocs.org/docsets/Locksmith)
## Installation
### CocoaPods
@@ -16,89 +21,421 @@ A sane way to work with the iOS Keychain in Swift.
Locksmith is available through [CocoaPods](http://cocoapods.org). To install
Locksmith for Swift 2, simply add the following line to your Podfile:
pod 'Locksmith', :git => 'https://github.com/matthewpalmer/Locksmith.git', :branch => '2.0'
pod 'Locksmith'
> Swift 1.2 support is available via the `1.2.2` branch.
### Manual
Alternatively, you can simply drag the two files `Locksmith.swift` and `LocksmithRequest.swift` into your project.
## Quick Start
In the following examples, you can choose not to provide a value for the `inService` parameter, and it will default to your Bundle Identifier.
## Quick start
**Save data**
```swift
try Locksmith.saveData(["some key": "some value"], forUserAccount: "myUserAccount", inService: "myService")
try Locksmith.saveData(["some key": "some value"], forUserAccount: "myUserAccount")
```
**Load data**
```swift
let dictionary = Locksmith.loadDataForUserAccount("myUserAccount", inService: "myService")
let dictionary = Locksmith.loadDataForUserAccount("myUserAccount")
```
**Update data**
```swift
try Locksmith.updateData(["some key": "another value"], forUserAccount: "myUserAccount", inService: "myService")
try Locksmith.updateData(["some key": "another value"], forUserAccount: "myUserAccount")
```
**Delete data**
```swift
try Locksmith.deleteDataForUserAccount("myUserAccount", inService: "myService")
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
// As above, the `service` parameter will default to your Bundle Identifier if omitted.
let saveRequest = LocksmithRequest(userAccount: userAccount, data: ["some key": "some value"], service: service)
// Customize the request
saveRequest.synchronizable = true
try 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(userAccount: userAccount, service: service)
let dictionary = try 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(userAccount: userAccount, requestType: .Delete, service: service)
try 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 youve gotta do,” and ”whats 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 its 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—its 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 cant 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!
Lets start with an example: the Twitter account from before, except its 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 lets 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
}
```
## Testing
I can't work out why, but opening `Example/Locksmith.xcworkspace` and trying to run the tests from there won't work. (Pull requests greatly appreciated on this!) Instead, you can run the tests by opening `Locksmith.xcodeproj` in the root directory, and doing Product -> Test.
## Author
[Matthew Palmer](http://matthewpalmer.net), matt@matthewpalmer.net