Add the app
This commit is contained in:
parent
5da64ead41
commit
6d19c4b81d
|
@ -0,0 +1,78 @@
|
|||
.DS_Store
|
||||
MyPlayground.playground/
|
||||
|
||||
### Swift ###
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## Build generated
|
||||
build/
|
||||
DerivedData/
|
||||
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata/
|
||||
|
||||
## Other
|
||||
*.moved-aside
|
||||
*.xccheckout
|
||||
*.xcscmblueprint
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
.build/
|
||||
|
||||
# CocoaPods - Refactored to standalone file
|
||||
|
||||
# Carthage - Refactored to standalone file
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
|
||||
### Xcode ###
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## Build generated
|
||||
|
||||
## Various settings
|
||||
|
||||
## Other
|
||||
|
||||
### Xcode Patch ###
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
!*.xcodeproj/xcshareddata/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
/*.gcno
|
|
@ -7,23 +7,119 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
D60E9D6E26D1998B009A4537 /* OTPGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E9D6D26D1998B009A4537 /* OTPGeneratorTests.swift */; };
|
||||
D60E9D7226D1A863009A4537 /* KeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E9D7126D1A863009A4537 /* KeyView.swift */; };
|
||||
D60E9D7426D1ABF9009A4537 /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E9D7326D1ABF9009A4537 /* CircularProgressView.swift */; };
|
||||
D60E9D7726D1B160009A4537 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = D60E9D7626D1B160009A4537 /* CodeScanner */; };
|
||||
D60E9D7A26D1E985009A4537 /* EditKeyForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E9D7926D1E985009A4537 /* EditKeyForm.swift */; };
|
||||
D61479D726D0AF1C00710B79 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61479D626D0AF1C00710B79 /* AppDelegate.swift */; };
|
||||
D61479D926D0AF1C00710B79 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61479D826D0AF1C00710B79 /* SceneDelegate.swift */; };
|
||||
D61479DB26D0AF1C00710B79 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61479DA26D0AF1C00710B79 /* ViewController.swift */; };
|
||||
D61479DE26D0AF1C00710B79 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D61479DC26D0AF1C00710B79 /* Main.storyboard */; };
|
||||
D61479E026D0AF1E00710B79 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D61479DF26D0AF1E00710B79 /* Assets.xcassets */; };
|
||||
D61479E326D0AF1E00710B79 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D61479E126D0AF1E00710B79 /* LaunchScreen.storyboard */; };
|
||||
D61479EB26D13DEF00710B79 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61479EA26D13DEF00710B79 /* AppView.swift */; };
|
||||
D61479F526D141F700710B79 /* OTPKit.docc in Sources */ = {isa = PBXBuildFile; fileRef = D61479F426D141F700710B79 /* OTPKit.docc */; };
|
||||
D61479FB26D141F800710B79 /* OTPKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61479F126D141F700710B79 /* OTPKit.framework */; };
|
||||
D6147A0226D141F800710B79 /* OTPKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6147A0126D141F800710B79 /* OTPKitTests.swift */; };
|
||||
D6147A0326D141F800710B79 /* OTPKit.h in Headers */ = {isa = PBXBuildFile; fileRef = D61479F326D141F700710B79 /* OTPKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
D6147A0626D141F800710B79 /* OTPKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61479F126D141F700710B79 /* OTPKit.framework */; };
|
||||
D6147A0726D141F800710B79 /* OTPKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D61479F126D141F700710B79 /* OTPKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
D6147A1026D1421900710B79 /* TOTPKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6147A0F26D1421900710B79 /* TOTPKey.swift */; };
|
||||
D6147A1326D1446E00710B79 /* Base32.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6147A1226D1446E00710B79 /* Base32.swift */; };
|
||||
D6147A1726D147B400710B79 /* TOTPKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6147A1626D147B400710B79 /* TOTPKeyTests.swift */; };
|
||||
D6147A1926D14AAB00710B79 /* OTPGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6147A1826D14AAB00710B79 /* OTPGenerator.swift */; };
|
||||
D6147A1B26D14ADF00710B79 /* OTPKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6147A1A26D14ADF00710B79 /* OTPKey.swift */; };
|
||||
D6147A1D26D14B5F00710B79 /* OTPAlgorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6147A1C26D14B5F00710B79 /* OTPAlgorithm.swift */; };
|
||||
D6147A1F26D14BA000710B79 /* TOTPCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6147A1E26D14BA000710B79 /* TOTPCode.swift */; };
|
||||
D68B3DA026D2937000CB35A2 /* AddURLForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68B3D9F26D2937000CB35A2 /* AddURLForm.swift */; };
|
||||
D68B3DA226D293BE00CB35A2 /* DismissAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68B3DA126D293BE00CB35A2 /* DismissAction.swift */; };
|
||||
D6A2BCAE26D29601004DC4E3 /* EditedKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A2BCAD26D29601004DC4E3 /* EditedKey.swift */; };
|
||||
D6A2BCB026D313E7004DC4E3 /* AddQRView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A2BCAF26D313E7004DC4E3 /* AddQRView.swift */; };
|
||||
D6A2BCCC26D3E670004DC4E3 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A2BCCB26D3E670004DC4E3 /* PreferencesView.swift */; };
|
||||
D6A2BCD026D3E888004DC4E3 /* BackupDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A2BCCF26D3E888004DC4E3 /* BackupDocument.swift */; };
|
||||
D6A2BCEC26D3F7F9004DC4E3 /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A2BCEB26D3F7F9004DC4E3 /* QRCodeView.swift */; };
|
||||
D6A2BD0826D449FA004DC4E3 /* KeyData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A2BD0726D449FA004DC4E3 /* KeyData.swift */; };
|
||||
D6A2BD0A26D44AC3004DC4E3 /* KeyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A2BD0926D44AC3004DC4E3 /* KeyStore.swift */; };
|
||||
D6A2BD0C26D5332B004DC4E3 /* FolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A2BD0B26D5332B004DC4E3 /* FolderView.swift */; };
|
||||
D6A2BD1026D533EB004DC4E3 /* FoldersSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A2BD0F26D533EB004DC4E3 /* FoldersSection.swift */; };
|
||||
D6A2BD1226D56C70004DC4E3 /* KeysSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A2BD1126D56C70004DC4E3 /* KeysSection.swift */; };
|
||||
D6A2BD1426D5707F004DC4E3 /* FolderRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A2BD1326D5707F004DC4E3 /* FolderRow.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
D61479FC26D141F800710B79 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D61479CB26D0AF1C00710B79 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = D61479F026D141F700710B79;
|
||||
remoteInfo = OTPKit;
|
||||
};
|
||||
D61479FE26D141F800710B79 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D61479CB26D0AF1C00710B79 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = D61479D226D0AF1C00710B79;
|
||||
remoteInfo = OTP;
|
||||
};
|
||||
D6147A0426D141F800710B79 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D61479CB26D0AF1C00710B79 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = D61479F026D141F700710B79;
|
||||
remoteInfo = OTPKit;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
D6147A0B26D141F800710B79 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
D6147A0726D141F800710B79 /* OTPKit.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
D60E9D6D26D1998B009A4537 /* OTPGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPGeneratorTests.swift; sourceTree = "<group>"; };
|
||||
D60E9D7126D1A863009A4537 /* KeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyView.swift; sourceTree = "<group>"; };
|
||||
D60E9D7326D1ABF9009A4537 /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = "<group>"; };
|
||||
D60E9D7926D1E985009A4537 /* EditKeyForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditKeyForm.swift; sourceTree = "<group>"; };
|
||||
D61479D326D0AF1C00710B79 /* OTP.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OTP.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D61479D626D0AF1C00710B79 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
D61479D826D0AF1C00710B79 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
D61479DA26D0AF1C00710B79 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
D61479DD26D0AF1C00710B79 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
D61479DF26D0AF1E00710B79 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
D61479E226D0AF1E00710B79 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
D61479E426D0AF1E00710B79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D61479EA26D13DEF00710B79 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
|
||||
D61479F126D141F700710B79 /* OTPKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OTPKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D61479F326D141F700710B79 /* OTPKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OTPKit.h; sourceTree = "<group>"; };
|
||||
D61479F426D141F700710B79 /* OTPKit.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = OTPKit.docc; sourceTree = "<group>"; };
|
||||
D61479FA26D141F800710B79 /* OTPKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OTPKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D6147A0126D141F800710B79 /* OTPKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPKitTests.swift; sourceTree = "<group>"; };
|
||||
D6147A0F26D1421900710B79 /* TOTPKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPKey.swift; sourceTree = "<group>"; };
|
||||
D6147A1226D1446E00710B79 /* Base32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base32.swift; sourceTree = "<group>"; };
|
||||
D6147A1626D147B400710B79 /* TOTPKeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPKeyTests.swift; sourceTree = "<group>"; };
|
||||
D6147A1826D14AAB00710B79 /* OTPGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPGenerator.swift; sourceTree = "<group>"; };
|
||||
D6147A1A26D14ADF00710B79 /* OTPKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPKey.swift; sourceTree = "<group>"; };
|
||||
D6147A1C26D14B5F00710B79 /* OTPAlgorithm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPAlgorithm.swift; sourceTree = "<group>"; };
|
||||
D6147A1E26D14BA000710B79 /* TOTPCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPCode.swift; sourceTree = "<group>"; };
|
||||
D68B3D9F26D2937000CB35A2 /* AddURLForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddURLForm.swift; sourceTree = "<group>"; };
|
||||
D68B3DA126D293BE00CB35A2 /* DismissAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissAction.swift; sourceTree = "<group>"; };
|
||||
D6A2BCAD26D29601004DC4E3 /* EditedKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditedKey.swift; sourceTree = "<group>"; };
|
||||
D6A2BCAF26D313E7004DC4E3 /* AddQRView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddQRView.swift; sourceTree = "<group>"; };
|
||||
D6A2BCCB26D3E670004DC4E3 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||
D6A2BCCF26D3E888004DC4E3 /* BackupDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupDocument.swift; sourceTree = "<group>"; };
|
||||
D6A2BCEB26D3F7F9004DC4E3 /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
|
||||
D6A2BD0726D449FA004DC4E3 /* KeyData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyData.swift; sourceTree = "<group>"; };
|
||||
D6A2BD0926D44AC3004DC4E3 /* KeyStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyStore.swift; sourceTree = "<group>"; };
|
||||
D6A2BD0B26D5332B004DC4E3 /* FolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderView.swift; sourceTree = "<group>"; };
|
||||
D6A2BD0F26D533EB004DC4E3 /* FoldersSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoldersSection.swift; sourceTree = "<group>"; };
|
||||
D6A2BD1126D56C70004DC4E3 /* KeysSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeysSection.swift; sourceTree = "<group>"; };
|
||||
D6A2BD1326D5707F004DC4E3 /* FolderRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderRow.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -31,16 +127,54 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D60E9D7726D1B160009A4537 /* CodeScanner in Frameworks */,
|
||||
D6147A0626D141F800710B79 /* OTPKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D61479EE26D141F700710B79 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D61479F726D141F800710B79 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D61479FB26D141F800710B79 /* OTPKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
D60E9D7826D1E976009A4537 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D61479EA26D13DEF00710B79 /* AppView.swift */,
|
||||
D6A2BD1126D56C70004DC4E3 /* KeysSection.swift */,
|
||||
D6A2BD0F26D533EB004DC4E3 /* FoldersSection.swift */,
|
||||
D6A2BD1326D5707F004DC4E3 /* FolderRow.swift */,
|
||||
D60E9D7126D1A863009A4537 /* KeyView.swift */,
|
||||
D60E9D7326D1ABF9009A4537 /* CircularProgressView.swift */,
|
||||
D6A2BD0B26D5332B004DC4E3 /* FolderView.swift */,
|
||||
D6A2BCAF26D313E7004DC4E3 /* AddQRView.swift */,
|
||||
D68B3D9F26D2937000CB35A2 /* AddURLForm.swift */,
|
||||
D60E9D7926D1E985009A4537 /* EditKeyForm.swift */,
|
||||
D6A2BCCB26D3E670004DC4E3 /* PreferencesView.swift */,
|
||||
D6A2BCEB26D3F7F9004DC4E3 /* QRCodeView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D61479CA26D0AF1C00710B79 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D61479D526D0AF1C00710B79 /* OTP */,
|
||||
D61479F226D141F700710B79 /* OTPKit */,
|
||||
D6147A0026D141F800710B79 /* OTPKitTests */,
|
||||
D61479D426D0AF1C00710B79 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
|
@ -49,6 +183,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D61479D326D0AF1C00710B79 /* OTP.app */,
|
||||
D61479F126D141F700710B79 /* OTPKit.framework */,
|
||||
D61479FA26D141F800710B79 /* OTPKitTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
@ -56,19 +192,67 @@
|
|||
D61479D526D0AF1C00710B79 /* OTP */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D61479E426D0AF1E00710B79 /* Info.plist */,
|
||||
D61479D626D0AF1C00710B79 /* AppDelegate.swift */,
|
||||
D6A2BCCF26D3E888004DC4E3 /* BackupDocument.swift */,
|
||||
D68B3DA126D293BE00CB35A2 /* DismissAction.swift */,
|
||||
D6A2BCAD26D29601004DC4E3 /* EditedKey.swift */,
|
||||
D6A2BD0726D449FA004DC4E3 /* KeyData.swift */,
|
||||
D6A2BD0926D44AC3004DC4E3 /* KeyStore.swift */,
|
||||
D61479D826D0AF1C00710B79 /* SceneDelegate.swift */,
|
||||
D61479DA26D0AF1C00710B79 /* ViewController.swift */,
|
||||
D61479DC26D0AF1C00710B79 /* Main.storyboard */,
|
||||
D61479DF26D0AF1E00710B79 /* Assets.xcassets */,
|
||||
D61479E126D0AF1E00710B79 /* LaunchScreen.storyboard */,
|
||||
D61479E426D0AF1E00710B79 /* Info.plist */,
|
||||
D60E9D7826D1E976009A4537 /* Views */,
|
||||
);
|
||||
path = OTP;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D61479F226D141F700710B79 /* OTPKit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D61479F426D141F700710B79 /* OTPKit.docc */,
|
||||
D61479F326D141F700710B79 /* OTPKit.h */,
|
||||
D6147A1C26D14B5F00710B79 /* OTPAlgorithm.swift */,
|
||||
D6147A1826D14AAB00710B79 /* OTPGenerator.swift */,
|
||||
D6147A1A26D14ADF00710B79 /* OTPKey.swift */,
|
||||
D6147A1E26D14BA000710B79 /* TOTPCode.swift */,
|
||||
D6147A0F26D1421900710B79 /* TOTPKey.swift */,
|
||||
D6147A1126D1446500710B79 /* Vendor */,
|
||||
);
|
||||
path = OTPKit;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6147A0026D141F800710B79 /* OTPKitTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6147A0126D141F800710B79 /* OTPKitTests.swift */,
|
||||
D60E9D6D26D1998B009A4537 /* OTPGeneratorTests.swift */,
|
||||
D6147A1626D147B400710B79 /* TOTPKeyTests.swift */,
|
||||
);
|
||||
path = OTPKitTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6147A1126D1446500710B79 /* Vendor */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6147A1226D1446E00710B79 /* Base32.swift */,
|
||||
);
|
||||
path = Vendor;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
D61479EC26D141F700710B79 /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D6147A0326D141F800710B79 /* OTPKit.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXHeadersBuildPhase section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
D61479D226D0AF1C00710B79 /* OTP */ = {
|
||||
isa = PBXNativeTarget;
|
||||
|
@ -77,15 +261,57 @@
|
|||
D61479CF26D0AF1C00710B79 /* Sources */,
|
||||
D61479D026D0AF1C00710B79 /* Frameworks */,
|
||||
D61479D126D0AF1C00710B79 /* Resources */,
|
||||
D6147A0B26D141F800710B79 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
D6147A0526D141F800710B79 /* PBXTargetDependency */,
|
||||
);
|
||||
name = OTP;
|
||||
packageProductDependencies = (
|
||||
D60E9D7626D1B160009A4537 /* CodeScanner */,
|
||||
);
|
||||
productName = OTP;
|
||||
productReference = D61479D326D0AF1C00710B79 /* OTP.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
D61479F026D141F700710B79 /* OTPKit */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = D6147A0826D141F800710B79 /* Build configuration list for PBXNativeTarget "OTPKit" */;
|
||||
buildPhases = (
|
||||
D61479EC26D141F700710B79 /* Headers */,
|
||||
D61479ED26D141F700710B79 /* Sources */,
|
||||
D61479EE26D141F700710B79 /* Frameworks */,
|
||||
D61479EF26D141F700710B79 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = OTP;
|
||||
productName = OTP;
|
||||
productReference = D61479D326D0AF1C00710B79 /* OTP.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
name = OTPKit;
|
||||
productName = OTPKit;
|
||||
productReference = D61479F126D141F700710B79 /* OTPKit.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
D61479F926D141F800710B79 /* OTPKitTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = D6147A0C26D141F800710B79 /* Build configuration list for PBXNativeTarget "OTPKitTests" */;
|
||||
buildPhases = (
|
||||
D61479F626D141F800710B79 /* Sources */,
|
||||
D61479F726D141F800710B79 /* Frameworks */,
|
||||
D61479F826D141F800710B79 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
D61479FD26D141F800710B79 /* PBXTargetDependency */,
|
||||
D61479FF26D141F800710B79 /* PBXTargetDependency */,
|
||||
);
|
||||
name = OTPKitTests;
|
||||
productName = OTPKitTests;
|
||||
productReference = D61479FA26D141F800710B79 /* OTPKitTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
|
@ -100,6 +326,13 @@
|
|||
D61479D226D0AF1C00710B79 = {
|
||||
CreatedOnToolsVersion = 13.0;
|
||||
};
|
||||
D61479F026D141F700710B79 = {
|
||||
CreatedOnToolsVersion = 13.0;
|
||||
};
|
||||
D61479F926D141F800710B79 = {
|
||||
CreatedOnToolsVersion = 13.0;
|
||||
TestTargetID = D61479D226D0AF1C00710B79;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = D61479CE26D0AF1C00710B79 /* Build configuration list for PBXProject "OTP" */;
|
||||
|
@ -111,11 +344,16 @@
|
|||
Base,
|
||||
);
|
||||
mainGroup = D61479CA26D0AF1C00710B79;
|
||||
packageReferences = (
|
||||
D60E9D7526D1B160009A4537 /* XCRemoteSwiftPackageReference "CodeScanner" */,
|
||||
);
|
||||
productRefGroup = D61479D426D0AF1C00710B79 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
D61479D226D0AF1C00710B79 /* OTP */,
|
||||
D61479F026D141F700710B79 /* OTPKit */,
|
||||
D61479F926D141F800710B79 /* OTPKitTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
@ -127,7 +365,20 @@
|
|||
files = (
|
||||
D61479E326D0AF1E00710B79 /* LaunchScreen.storyboard in Resources */,
|
||||
D61479E026D0AF1E00710B79 /* Assets.xcassets in Resources */,
|
||||
D61479DE26D0AF1C00710B79 /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D61479EF26D141F700710B79 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D61479F826D141F800710B79 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -138,23 +389,73 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D61479DB26D0AF1C00710B79 /* ViewController.swift in Sources */,
|
||||
D61479D726D0AF1C00710B79 /* AppDelegate.swift in Sources */,
|
||||
D6A2BCCC26D3E670004DC4E3 /* PreferencesView.swift in Sources */,
|
||||
D6A2BD0C26D5332B004DC4E3 /* FolderView.swift in Sources */,
|
||||
D61479D926D0AF1C00710B79 /* SceneDelegate.swift in Sources */,
|
||||
D6A2BD1026D533EB004DC4E3 /* FoldersSection.swift in Sources */,
|
||||
D6A2BCAE26D29601004DC4E3 /* EditedKey.swift in Sources */,
|
||||
D6A2BD1426D5707F004DC4E3 /* FolderRow.swift in Sources */,
|
||||
D6A2BCD026D3E888004DC4E3 /* BackupDocument.swift in Sources */,
|
||||
D6A2BD1226D56C70004DC4E3 /* KeysSection.swift in Sources */,
|
||||
D60E9D7226D1A863009A4537 /* KeyView.swift in Sources */,
|
||||
D68B3DA026D2937000CB35A2 /* AddURLForm.swift in Sources */,
|
||||
D61479EB26D13DEF00710B79 /* AppView.swift in Sources */,
|
||||
D68B3DA226D293BE00CB35A2 /* DismissAction.swift in Sources */,
|
||||
D60E9D7426D1ABF9009A4537 /* CircularProgressView.swift in Sources */,
|
||||
D6A2BCEC26D3F7F9004DC4E3 /* QRCodeView.swift in Sources */,
|
||||
D60E9D7A26D1E985009A4537 /* EditKeyForm.swift in Sources */,
|
||||
D6A2BD0A26D44AC3004DC4E3 /* KeyStore.swift in Sources */,
|
||||
D6A2BCB026D313E7004DC4E3 /* AddQRView.swift in Sources */,
|
||||
D6A2BD0826D449FA004DC4E3 /* KeyData.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D61479ED26D141F700710B79 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D61479F526D141F700710B79 /* OTPKit.docc in Sources */,
|
||||
D6147A1F26D14BA000710B79 /* TOTPCode.swift in Sources */,
|
||||
D6147A1326D1446E00710B79 /* Base32.swift in Sources */,
|
||||
D6147A1026D1421900710B79 /* TOTPKey.swift in Sources */,
|
||||
D6147A1D26D14B5F00710B79 /* OTPAlgorithm.swift in Sources */,
|
||||
D6147A1926D14AAB00710B79 /* OTPGenerator.swift in Sources */,
|
||||
D6147A1B26D14ADF00710B79 /* OTPKey.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D61479F626D141F800710B79 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D6147A1726D147B400710B79 /* TOTPKeyTests.swift in Sources */,
|
||||
D6147A0226D141F800710B79 /* OTPKitTests.swift in Sources */,
|
||||
D60E9D6E26D1998B009A4537 /* OTPGeneratorTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
D61479DC26D0AF1C00710B79 /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
D61479DD26D0AF1C00710B79 /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
/* Begin PBXTargetDependency section */
|
||||
D61479FD26D141F800710B79 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D61479F026D141F700710B79 /* OTPKit */;
|
||||
targetProxy = D61479FC26D141F800710B79 /* PBXContainerItemProxy */;
|
||||
};
|
||||
D61479FF26D141F800710B79 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D61479D226D0AF1C00710B79 /* OTP */;
|
||||
targetProxy = D61479FE26D141F800710B79 /* PBXContainerItemProxy */;
|
||||
};
|
||||
D6147A0526D141F800710B79 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D61479F026D141F700710B79 /* OTPKit */;
|
||||
targetProxy = D6147A0426D141F800710B79 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
D61479E126D0AF1E00710B79 /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
|
@ -285,6 +586,7 @@
|
|||
D61479E826D0AF1E00710B79 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
@ -294,9 +596,9 @@
|
|||
INFOPLIST_FILE = OTP/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -313,6 +615,7 @@
|
|||
D61479E926D0AF1E00710B79 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
@ -322,9 +625,9 @@
|
|||
INFOPLIST_FILE = OTP/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -338,6 +641,112 @@
|
|||
};
|
||||
name = Release;
|
||||
};
|
||||
D6147A0926D141F800710B79 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.OTPKit;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
D6147A0A26D141F800710B79 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.OTPKit;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
D6147A0D26D141F800710B79 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.OTPKitTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/OTP.app/OTP";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
D6147A0E26D141F800710B79 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.OTPKitTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/OTP.app/OTP";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
|
@ -359,7 +768,44 @@
|
|||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
D6147A0826D141F800710B79 /* Build configuration list for PBXNativeTarget "OTPKit" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
D6147A0926D141F800710B79 /* Debug */,
|
||||
D6147A0A26D141F800710B79 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
D6147A0C26D141F800710B79 /* Build configuration list for PBXNativeTarget "OTPKitTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
D6147A0D26D141F800710B79 /* Debug */,
|
||||
D6147A0E26D141F800710B79 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
D60E9D7526D1B160009A4537 /* XCRemoteSwiftPackageReference "CodeScanner" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/twostraws/CodeScanner";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.0.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
D60E9D7626D1B160009A4537 /* CodeScanner */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D60E9D7526D1B160009A4537 /* XCRemoteSwiftPackageReference "CodeScanner" */;
|
||||
productName = CodeScanner;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = D61479CB26D0AF1C00710B79 /* Project object */;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,11 @@
|
|||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>OTP.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>OTPKit.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
|
|
|
@ -11,9 +11,7 @@ import UIKit
|
|||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// BackupDocument.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/23/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import OTPKit
|
||||
|
||||
struct BackupDocument: FileDocument {
|
||||
|
||||
private static let encoder = PropertyListEncoder()
|
||||
private static let decoder = PropertyListDecoder()
|
||||
|
||||
static var readableContentTypes: [UTType] { [.propertyList] }
|
||||
|
||||
private(set) var data: KeyData
|
||||
|
||||
init(data: KeyData) {
|
||||
self.data = data
|
||||
}
|
||||
|
||||
init(configuration: ReadConfiguration) throws {
|
||||
if let data = configuration.file.regularFileContents {
|
||||
let store = try BackupDocument.decoder.decode(KeyData.self, from: data)
|
||||
self.data = store
|
||||
} else {
|
||||
self.data = KeyData()
|
||||
}
|
||||
}
|
||||
|
||||
init(url: URL) throws {
|
||||
let data = try Data(contentsOf: url)
|
||||
let store = try BackupDocument.decoder.decode(KeyData.self, from: data)
|
||||
self.data = store
|
||||
}
|
||||
|
||||
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
|
||||
let data = try! BackupDocument.encoder.encode(data)
|
||||
return FileWrapper(regularFileWithContents: data)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// DismissAction.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/22/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OTPKit
|
||||
|
||||
enum DismissAction {
|
||||
case cancel
|
||||
case save(TOTPKey)
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
//
|
||||
// EditedKey.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/22/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OTPKit
|
||||
|
||||
struct EditedKey {
|
||||
var secret: String
|
||||
var period: Period
|
||||
var digits: Digits
|
||||
var issuer: String
|
||||
var label: String
|
||||
|
||||
init() {
|
||||
self.secret = ""
|
||||
self.period = .thirty
|
||||
self.digits = .six
|
||||
self.issuer = ""
|
||||
self.label = ""
|
||||
}
|
||||
|
||||
init?(totpKey: TOTPKey) {
|
||||
guard totpKey.period == 30 || totpKey.period == 60,
|
||||
totpKey.digits == 6 || totpKey.digits == 8 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.secret = totpKey.secret.base32EncodedString
|
||||
self.period = totpKey.period == 30 ? .thirty : .sixty
|
||||
self.digits = totpKey.digits == 6 ? .six : .eight
|
||||
self.issuer = totpKey.issuer
|
||||
self.label = totpKey.label ?? ""
|
||||
}
|
||||
|
||||
func toTOTPKey() -> TOTPKey? {
|
||||
let secretStr = self.secret.replacingOccurrences(of: " ", with: "")
|
||||
guard secretStr.count > 0,
|
||||
let secret = secretStr.base32DecodedData,
|
||||
!issuer.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return TOTPKey(secret: secret, period: period.value, digits: digits.value, label: label.trimmingCharacters(in: .whitespacesAndNewlines), issuer: issuer)
|
||||
}
|
||||
|
||||
enum Period: Hashable {
|
||||
case thirty, sixty
|
||||
|
||||
var value: Int {
|
||||
switch self {
|
||||
case .thirty:
|
||||
return 30
|
||||
case .sixty:
|
||||
return 60
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Digits: Hashable {
|
||||
case six, eight
|
||||
|
||||
var value: Int {
|
||||
switch self {
|
||||
case .six:
|
||||
return 6
|
||||
case .eight:
|
||||
return 8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,10 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Save exported QR codes</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Scan OTP keys from QR codes</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
|
@ -15,8 +19,6 @@
|
|||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
//
|
||||
// KeyData.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/23/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OTPKit
|
||||
|
||||
struct KeyData: Codable {
|
||||
|
||||
private(set) var entries: [Entry] = []
|
||||
private(set) var folders: [Folder] = []
|
||||
|
||||
init(entries: [Entry] = []) {
|
||||
self.entries = entries
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.entries = try container.decode([Entry].self, forKey: .entries)
|
||||
self.folders = try container.decode([Folder].self, forKey: .folders)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(entries, forKey: .entries)
|
||||
try container.encode(folders, forKey: .folders)
|
||||
}
|
||||
|
||||
mutating func addKey(_ key: TOTPKey) {
|
||||
entries.append(Entry(key: key))
|
||||
}
|
||||
|
||||
mutating func addOrUpdateEntries(_ entries: [Entry]) {
|
||||
for e in entries {
|
||||
if let index = self.entries.firstIndex(where: { $0.id == e.id }) {
|
||||
self.entries[index] = e
|
||||
} else {
|
||||
self.entries.append(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func updateKey(entryID id: UUID, newKey: TOTPKey) {
|
||||
guard let index = entries.firstIndex(where: { $0.id == id }) else {
|
||||
return
|
||||
}
|
||||
entries[index].key = newKey
|
||||
}
|
||||
|
||||
mutating func removeKey(entryID id: UUID) {
|
||||
entries.removeAll(where: { $0.id == id })
|
||||
}
|
||||
|
||||
mutating func clearEntries() {
|
||||
entries.removeAll()
|
||||
}
|
||||
|
||||
mutating func addFolder() {
|
||||
let count = folders.filter { $0.name.starts(with: "New Folder") }.count
|
||||
if count > 0 {
|
||||
folders.append(Folder(name: "New Folder \(count + 1)"))
|
||||
} else {
|
||||
folders.append(Folder(name: "New Folder"))
|
||||
}
|
||||
}
|
||||
|
||||
mutating func updateFolder(_ newFolder: KeyData.Folder) {
|
||||
guard let index = folders.firstIndex(where: { $0.id == newFolder.id }) else {
|
||||
return
|
||||
}
|
||||
folders[index] = newFolder
|
||||
}
|
||||
|
||||
mutating func removeFolder(id: UUID) {
|
||||
folders.removeAll(where: { $0.id == id })
|
||||
for (index, e) in entries.enumerated() where e.folderID == id {
|
||||
entries[index].folderID = nil
|
||||
}
|
||||
}
|
||||
|
||||
mutating func moveEntryToFolder(entryID: UUID, folderID: UUID?) {
|
||||
guard let index = entries.firstIndex(where: { $0.id == entryID }),
|
||||
folderID == nil || folders.contains(where: { $0.id == folderID }) else {
|
||||
return
|
||||
}
|
||||
entries[index].folderID = folderID
|
||||
}
|
||||
|
||||
struct Entry: Identifiable, Hashable, Codable {
|
||||
let id: UUID
|
||||
var key: TOTPKey
|
||||
var folderID: UUID?
|
||||
let image: Data?
|
||||
|
||||
init(id: UUID = UUID(), key: TOTPKey, folderID: UUID? = nil, image: Data? = nil) {
|
||||
self.id = id
|
||||
self.key = key
|
||||
self.folderID = folderID
|
||||
self.image = image
|
||||
}
|
||||
}
|
||||
|
||||
struct Folder: Identifiable, Codable {
|
||||
let id: UUID
|
||||
var name: String
|
||||
|
||||
init(id: UUID = UUID(), name: String) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: CodingKey {
|
||||
case entries
|
||||
case folders
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
//
|
||||
// KeyStore.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/23/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OTPKit
|
||||
import Security
|
||||
import Combine
|
||||
|
||||
class KeyStore: ObservableObject {
|
||||
static let shared = try! KeyStore()
|
||||
|
||||
private lazy var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
private lazy var storeArchiveURL = documentsDirectory.appendingPathComponent("KeyStore").appendingPathExtension("plist")
|
||||
|
||||
private let encoder = PropertyListEncoder()
|
||||
private let decoder = PropertyListDecoder()
|
||||
|
||||
@Published private(set) var data: KeyData! {
|
||||
didSet {
|
||||
let data = try! encoder.encode(data)
|
||||
try! data.write(to: storeArchiveURL, options: .completeFileProtectionUntilFirstUserAuthentication)
|
||||
}
|
||||
}
|
||||
|
||||
var entries: [KeyData.Entry] {
|
||||
data.entries
|
||||
}
|
||||
|
||||
var folders: [KeyData.Folder] {
|
||||
data.folders
|
||||
}
|
||||
|
||||
var sortedFolders: [KeyData.Folder] {
|
||||
data.folders.sorted(by: { (a, b) in a.name < b.name })
|
||||
}
|
||||
|
||||
private init() throws {
|
||||
if let data = try? Data(contentsOf: storeArchiveURL) {
|
||||
self.data = try decoder.decode(KeyData.self, from: data)
|
||||
} else {
|
||||
self.data = KeyData()
|
||||
}
|
||||
}
|
||||
|
||||
func updateFromStore(_ newStore: KeyData, replaceExisting: Bool) {
|
||||
if replaceExisting {
|
||||
data = newStore
|
||||
} else {
|
||||
data.addOrUpdateEntries(newStore.entries)
|
||||
}
|
||||
}
|
||||
|
||||
func addKey(_ key: TOTPKey) {
|
||||
data.addKey(key)
|
||||
}
|
||||
|
||||
func updateKey(entryID id: UUID, newKey: TOTPKey) {
|
||||
data.updateKey(entryID: id, newKey: newKey)
|
||||
}
|
||||
|
||||
func removeKey(entryID id: UUID) {
|
||||
data.removeKey(entryID: id)
|
||||
}
|
||||
|
||||
func clearEntries() {
|
||||
data.clearEntries()
|
||||
}
|
||||
|
||||
func addFolder() {
|
||||
data.addFolder()
|
||||
}
|
||||
|
||||
func updateFolder(_ newFolder: KeyData.Folder) {
|
||||
data.updateFolder(newFolder)
|
||||
}
|
||||
|
||||
func removeFolder(id: UUID) {
|
||||
data.removeFolder(id: id)
|
||||
}
|
||||
|
||||
func moveEntryToFolder(entryID: UUID, folderID: UUID?) {
|
||||
data.moveEntryToFolder(entryID: entryID, folderID: folderID)
|
||||
}
|
||||
|
||||
}
|
|
@ -6,17 +6,23 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
|
||||
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
||||
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
||||
guard let _ = (scene as? UIWindowScene) else { return }
|
||||
guard let windowScene = scene as? UIWindowScene else { return }
|
||||
|
||||
window = UIWindow(windowScene: windowScene)
|
||||
|
||||
window!.rootViewController = UIHostingController(rootView: AppView())
|
||||
|
||||
window!.makeKeyAndVisible()
|
||||
}
|
||||
|
||||
func sceneDidDisconnect(_ scene: UIScene) {
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
//
|
||||
// ViewController.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/20/21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ViewController: UIViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
// Do any additional setup after loading the view.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
//
|
||||
// AddQRView.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/22/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CodeScanner
|
||||
import OTPKit
|
||||
|
||||
struct AddQRView: View {
|
||||
let dismiss: (DismissAction) -> Void
|
||||
@State private var isPresentingScanFailedAlert = false
|
||||
@State private var scanError: ScanError?
|
||||
@State private var scannedKey: TOTPKey?
|
||||
@State private var isShowingConfirmView = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
CodeScannerView(codeTypes: [.qr], scanMode: .once) { (result) in
|
||||
switch result {
|
||||
case .success(let code):
|
||||
if let components = URLComponents(string: code),
|
||||
let key = TOTPKey(urlComponents: components) {
|
||||
self.scannedKey = key
|
||||
isShowingConfirmView = true
|
||||
} else {
|
||||
isPresentingScanFailedAlert = true
|
||||
scanError = .invalidCode(code)
|
||||
}
|
||||
case .failure(let error):
|
||||
isPresentingScanFailedAlert = true
|
||||
scanError = .scanner(error)
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
.navigationBarTitle("Scan QR Code", displayMode: .inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
NavigationLink(isActive: $isShowingConfirmView) {
|
||||
EditKeyForm(editingKey: scannedKey, showCancelButton: false, dismiss: dismiss)
|
||||
.navigationTitle("Add Key")
|
||||
} label: {
|
||||
// EmptyView because this is only used to trigger programatic navigation
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.alert("Unable to get OTP key from QR code", isPresented: $isPresentingScanFailedAlert) {
|
||||
Button("Cancel", role: .cancel) {
|
||||
dismiss(.cancel)
|
||||
}
|
||||
Button("Try Again") {
|
||||
isPresentingScanFailedAlert = false
|
||||
}
|
||||
} message: {
|
||||
if let error = scanError {
|
||||
Text(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AddQRView {
|
||||
enum ScanError: LocalizedError {
|
||||
case scanner(CodeScannerView.ScanError)
|
||||
case invalidCode(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidCode(let code):
|
||||
return "Invalid Code: '\(code)'"
|
||||
case .scanner(.badInput):
|
||||
return "Scanner: Bad Input"
|
||||
case .scanner(.badOutput):
|
||||
return "Scanner: Bad Output"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AddQRView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddQRView() { (_) in }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
//
|
||||
// AddURLForm.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/22/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import OTPKit
|
||||
|
||||
struct AddURLForm: View {
|
||||
let dismiss: (DismissAction) -> Void
|
||||
@State private var inputURL = ""
|
||||
@State private var extractedKey = EditedKey()
|
||||
@FocusState private var urlFocused: Bool
|
||||
|
||||
private var isValid: Bool {
|
||||
extractedKey.toTOTPKey() != nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
HStack {
|
||||
TextField("URL", text: $inputURL)
|
||||
.focused($urlFocused)
|
||||
|
||||
Button {
|
||||
self.inputURL = UIPasteboard.general.string ?? ""
|
||||
} label: {
|
||||
Label("Paste", systemImage: "doc.on.clipboard")
|
||||
.labelStyle(.iconOnly)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!UIPasteboard.general.hasStrings)
|
||||
}
|
||||
|
||||
Section {
|
||||
TextField("Issuer", text: $extractedKey.issuer)
|
||||
TextField("Label", text: $extractedKey.label)
|
||||
TextField("Secret", text: $extractedKey.secret)
|
||||
} header: {
|
||||
Text("Extracted Key")
|
||||
}
|
||||
.disabled(true)
|
||||
}
|
||||
.onSubmit {
|
||||
if isValid {
|
||||
dismiss(.save(extractedKey.toTOTPKey()!))
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// doesn't work, see FB9551099
|
||||
urlFocused = true
|
||||
}
|
||||
.navigationTitle("Add from URL")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss(.cancel)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
dismiss(.save(extractedKey.toTOTPKey()!))
|
||||
}
|
||||
.disabled(!isValid)
|
||||
}
|
||||
}
|
||||
.onChange(of: inputURL, perform: self.updateExtractedKey(inputURL:))
|
||||
}
|
||||
|
||||
private func updateExtractedKey(inputURL: String) {
|
||||
let text = inputURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !text.isEmpty,
|
||||
let components = URLComponents(string: inputURL),
|
||||
let totpKey = TOTPKey(urlComponents: components),
|
||||
let edited = EditedKey(totpKey: totpKey) {
|
||||
extractedKey = edited
|
||||
} else {
|
||||
extractedKey = EditedKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AddURLForm_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddURLForm() { (_) in }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
//
|
||||
// AppView.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/21/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import OTPKit
|
||||
import Combine
|
||||
|
||||
struct AppView: View {
|
||||
@ObservedObject private var store: KeyStore
|
||||
@ObservedObject private var entryHolder: CodeHolder
|
||||
@State private var isPresentingScanner = false
|
||||
@State private var isPresentingScanFailedAlert = false
|
||||
@State private var isPresentingAddURLSheet = false
|
||||
@State private var isPresentingManualAddFormSheet = false
|
||||
@State private var isPresentingPreferences = false
|
||||
|
||||
init() {
|
||||
self.store = .shared
|
||||
self.entryHolder = CodeHolder(store: .shared) { (entry) in entry.folderID == nil }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
KeysSection(codeHolder: entryHolder)
|
||||
|
||||
FoldersSection()
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle("OTP")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
isPresentingPreferences = true
|
||||
} label: {
|
||||
Label("Preferences", systemImage: "gear")
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Menu {
|
||||
Section {
|
||||
Button {
|
||||
isPresentingScanner = true
|
||||
} label: {
|
||||
Label("Scan QR", systemImage: "qrcode.viewfinder")
|
||||
}
|
||||
Button {
|
||||
isPresentingAddURLSheet = true
|
||||
} label: {
|
||||
Label("From URL", systemImage: "link")
|
||||
}
|
||||
Button {
|
||||
isPresentingManualAddFormSheet = true
|
||||
} label: {
|
||||
Label("Enter Manually", systemImage: "textbox")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
store.addFolder()
|
||||
} label: {
|
||||
Label("New Folder", systemImage: "folder.badge.plus")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Add Key", systemImage: "plus.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.tint(.blue)
|
||||
.sheet(isPresented: $isPresentingPreferences, content: self.preferencesSheet)
|
||||
.sheet(isPresented: $isPresentingScanner, content: self.scannerSheet)
|
||||
.sheet(isPresented: $isPresentingManualAddFormSheet, content: self.manualAddFormSheet)
|
||||
.sheet(isPresented: $isPresentingAddURLSheet, content: self.addURLSheet)
|
||||
}
|
||||
|
||||
private func preferencesSheet() -> some View {
|
||||
NavigationView {
|
||||
PreferencesView()
|
||||
}
|
||||
}
|
||||
|
||||
private func scannerSheet() -> some View {
|
||||
AddQRView() { (action) in
|
||||
self.isPresentingScanner = false
|
||||
switch action {
|
||||
case .cancel:
|
||||
break
|
||||
case .save(let key):
|
||||
store.addKey(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addURLSheet() -> some View {
|
||||
NavigationView {
|
||||
AddURLForm { (action) in
|
||||
self.isPresentingAddURLSheet = false
|
||||
switch action {
|
||||
case .cancel:
|
||||
break
|
||||
case .save(let key):
|
||||
store.addKey(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func manualAddFormSheet() -> some View {
|
||||
NavigationView {
|
||||
EditKeyForm(editingKey: nil, focusOnAppear: true) { (action) in
|
||||
self.isPresentingManualAddFormSheet = false
|
||||
switch action {
|
||||
case .cancel:
|
||||
break
|
||||
case .save(let key):
|
||||
store.addKey(key)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Add Key")
|
||||
}
|
||||
}
|
||||
|
||||
struct CodeEntry: Identifiable, Equatable, Hashable {
|
||||
let entry: KeyData.Entry
|
||||
let code: TOTPCode
|
||||
var key: TOTPKey { entry.key }
|
||||
var id: UUID { entry.id }
|
||||
|
||||
init(_ entry: KeyData.Entry) {
|
||||
self.entry = entry
|
||||
self.code = OTPGenerator.generate(key: entry.key)
|
||||
}
|
||||
}
|
||||
|
||||
class CodeHolder: ObservableObject {
|
||||
private let store: KeyStore
|
||||
private let entryFilter: ((KeyData.Entry) -> Bool)?
|
||||
private var timer: Timer!
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(store: KeyStore, entryFilter: ((KeyData.Entry) -> Bool)? = nil) {
|
||||
self.store = store
|
||||
self.entryFilter = entryFilter
|
||||
|
||||
updateTimer(entries: filterEntries(from: store.data))
|
||||
|
||||
store.$data
|
||||
.sink { [unowned self] (newData) in
|
||||
self.objectWillChange.send()
|
||||
self.updateTimer(entries: filterEntries(from: newData!))
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
var entries: [CodeEntry] {
|
||||
return filterEntries(from: store.data).map { CodeEntry($0) }
|
||||
}
|
||||
|
||||
var sortedEntries: [CodeEntry] {
|
||||
return entries.sorted(by: { (a, b) in
|
||||
if a.key.issuer == b.key.issuer,
|
||||
let aLabel = a.key.label,
|
||||
let bLabel = b.key.label {
|
||||
return aLabel < bLabel
|
||||
} else {
|
||||
return a.key.issuer < b.key.issuer
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func filterEntries(from data: KeyData) -> [KeyData.Entry] {
|
||||
if let filter = entryFilter {
|
||||
return data.entries.filter(filter)
|
||||
} else {
|
||||
return data.entries
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTimer(entries: [KeyData.Entry]) {
|
||||
if entries.isEmpty {
|
||||
timer?.invalidate()
|
||||
return
|
||||
} else if timer == nil || !timer.isValid {
|
||||
timer = .scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] (timer) in
|
||||
guard let self = self else {
|
||||
timer.invalidate()
|
||||
return
|
||||
}
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
timer.tolerance = 0.01
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct AppView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AppView()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// CircularProgressView.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/21/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CircularProgressView: View {
|
||||
let progress: Double
|
||||
let colorChangeThreshold: Double
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(lineWidth: 4)
|
||||
.foregroundColor(progress <= colorChangeThreshold ? .red : .accentColor)
|
||||
.opacity(0.4)
|
||||
.animation(.default, value: progress)
|
||||
|
||||
Circle()
|
||||
.trim(from: 0, to: progress)
|
||||
.stroke(lineWidth: 4)
|
||||
.foregroundColor(progress <= colorChangeThreshold ? .red : .accentColor)
|
||||
.rotationEffect(.degrees(270))
|
||||
.animation(.default, value: progress)
|
||||
|
||||
}
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
}
|
||||
}
|
||||
|
||||
struct CircularProgressView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CircularProgressView(progress: 0.6, colorChangeThreshold: 0)
|
||||
.frame(width: 30)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
//
|
||||
// EditKeyForm.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/21/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import OTPKit
|
||||
|
||||
struct EditKeyForm: View {
|
||||
let dismiss: (DismissAction) -> Void
|
||||
let showCancelButton: Bool
|
||||
let focusOnAppear: Bool
|
||||
@State private var editedKey: EditedKey
|
||||
@FocusState private var issuerFocused: Bool
|
||||
|
||||
init(editingKey: TOTPKey?, showCancelButton: Bool = true, focusOnAppear: Bool = false, dismiss: @escaping (DismissAction) -> Void) {
|
||||
self.dismiss = dismiss
|
||||
self.showCancelButton = showCancelButton
|
||||
self.focusOnAppear = focusOnAppear
|
||||
if let totpKey = editingKey,
|
||||
let edited = EditedKey(totpKey: totpKey) {
|
||||
self.editedKey = edited
|
||||
} else {
|
||||
self.editedKey = EditedKey()
|
||||
}
|
||||
}
|
||||
|
||||
var isValid: Bool {
|
||||
editedKey.toTOTPKey() != nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
TextField("Issuer", text: $editedKey.issuer)
|
||||
.focused($issuerFocused)
|
||||
TextField("Account", text: $editedKey.label)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
|
||||
HStack {
|
||||
TextField("Secret", text: $editedKey.secret)
|
||||
Button {
|
||||
self.editedKey.secret = UIPasteboard.general.string ?? ""
|
||||
} label: {
|
||||
Label("Paste", systemImage: "doc.on.clipboard")
|
||||
.labelStyle(.iconOnly)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!UIPasteboard.general.hasStrings)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Period")
|
||||
Spacer()
|
||||
Picker("Period", selection: $editedKey.period) {
|
||||
Text("30 sec").tag(EditedKey.Period.thirty)
|
||||
Text("60 sec").tag(EditedKey.Period.sixty)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.frame(maxWidth: 200)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Digits")
|
||||
Spacer()
|
||||
Picker("Digits", selection: $editedKey.digits) {
|
||||
Text("6").tag(EditedKey.Digits.six)
|
||||
Text("8").tag(EditedKey.Digits.eight)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.frame(maxWidth: 200)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if focusOnAppear {
|
||||
// todo: this just doesn't work :/
|
||||
// https://developer.apple.com/forums/thread/681962
|
||||
// FB9551099
|
||||
issuerFocused = true
|
||||
}
|
||||
}
|
||||
.onSubmit {
|
||||
if isValid {
|
||||
dismiss(.save(editedKey.toTOTPKey()!))
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
// ToolbarContentBuilder doesn't support conditionals, so this has to go inside the ToolbarItem
|
||||
if showCancelButton {
|
||||
Button("Cancel") {
|
||||
dismiss(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
dismiss(.save(editedKey.toTOTPKey()!))
|
||||
}
|
||||
.disabled(!isValid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EditKeyForm_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EditKeyForm(editingKey: nil) { (_) in }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
//
|
||||
// FolderRow.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/24/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct FolderRow: View {
|
||||
@ObservedObject private var store: KeyStore
|
||||
@State private var folder: KeyData.Folder
|
||||
@State private var editing = false
|
||||
@FocusState private var focused: Bool
|
||||
@State private var isTargetedForDrop = false
|
||||
|
||||
init(store: KeyStore, folder: KeyData.Folder) {
|
||||
self.store = store
|
||||
self._folder = State(initialValue: folder)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationLink {
|
||||
FolderView(folder: folder)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "folder.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
if editing {
|
||||
TextField("Name", text: $folder.name)
|
||||
.focused($focused)
|
||||
.submitLabel(.done)
|
||||
.onSubmit {
|
||||
withAnimation {
|
||||
store.updateFolder(folder)
|
||||
}
|
||||
editing = false
|
||||
focused = false
|
||||
}
|
||||
} else {
|
||||
Text(folder.name)
|
||||
}
|
||||
}
|
||||
// .background(isTargetedForDrop ? Color.red : nil)
|
||||
// .onDrop(of: [.text], isTargeted: $isTargetedForDrop) { (itemProviders) in
|
||||
// guard let provider = itemProviders.first,
|
||||
// provider.canLoadObject(ofClass: NSString.self) else {
|
||||
// return false
|
||||
// }
|
||||
// provider.loadObject(ofClass: NSString.self) { (idString, error) in
|
||||
// guard error == nil,
|
||||
// let idString = idString as? NSString,
|
||||
// let id = UUID(uuidString: idString as String) else {
|
||||
// return
|
||||
// }
|
||||
// store.moveEntryToFolder(entryID: id, folderID: folder.id)
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
}
|
||||
.contextMenu {
|
||||
Button {
|
||||
editing = true
|
||||
// just waiting 1 runloop iteration does not work for some reason :/
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
focused = true
|
||||
}
|
||||
} label: {
|
||||
Label("Rename Folder", systemImage: "pencil")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FolderRow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
FolderRow(store: .shared, folder: .init(name: "Test"))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// FolderView.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/24/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct FolderView: View {
|
||||
@ObservedObject private var store: KeyStore
|
||||
private let folder: KeyData.Folder
|
||||
@ObservedObject private var codeHolder: AppView.CodeHolder
|
||||
|
||||
init(folder: KeyData.Folder) {
|
||||
let store = KeyStore.shared
|
||||
self.store = store
|
||||
self.folder = folder
|
||||
self.codeHolder = AppView.CodeHolder(store: store) { (entry) in
|
||||
entry.folderID == folder.id
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
KeysSection(codeHolder: codeHolder)
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle(folder.name)
|
||||
}
|
||||
}
|
||||
|
||||
struct FolderView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
FolderView(folder: .init(name: "Test"))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// FoldersSection.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/24/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct FoldersSection: View {
|
||||
@ObservedObject private var store: KeyStore = .shared
|
||||
@FocusState private var focusedEdited: Bool
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
ForEach(store.sortedFolders) { (folder) in
|
||||
FolderRow(store: store, folder: folder)
|
||||
}
|
||||
.onDelete { (indices) in
|
||||
let folderIDs = indices.map { store.sortedFolders[$0].id }
|
||||
withAnimation {
|
||||
folderIDs.forEach(store.removeFolder(id:))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FoldersSectionView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
FoldersSection()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// KeyView.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/21/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import OTPKit
|
||||
|
||||
struct KeyView: View {
|
||||
let key: TOTPKey
|
||||
let currentCode: TOTPCode
|
||||
|
||||
var formattedCode: String {
|
||||
let code = currentCode.code
|
||||
let mid = code.index(code.startIndex, offsetBy: code.count / 2)
|
||||
return "\(code[code.startIndex..<mid]) \(code[mid...])"
|
||||
}
|
||||
|
||||
init(key: TOTPKey, currentCode: TOTPCode) {
|
||||
self.key = key
|
||||
self.currentCode = currentCode
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(key.issuer)
|
||||
.font(.title3)
|
||||
|
||||
if let label = key.label, !label.isEmpty {
|
||||
Text(label)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(formattedCode)
|
||||
.font(.system(.title2, design: .monospaced))
|
||||
|
||||
// Text("\(currentCode.validUntil, style: .relative)")
|
||||
// .font(.body.monospacedDigit())
|
||||
|
||||
// I don't think this TimelineView should be necessary since the CodeHolder timer fires every .5 seconds
|
||||
TimelineView(.animation) { (ctx) in
|
||||
ZStack {
|
||||
CircularProgressView(progress: progress(at: Date()), colorChangeThreshold: 5.0 / Double(key.period))
|
||||
|
||||
Text(Int(round(currentCode.validUntil.timeIntervalSinceNow)).description)
|
||||
.font(.caption.monospacedDigit())
|
||||
}
|
||||
.frame(width: 30)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private func progress(at date: Date) -> Double {
|
||||
let seconds = round(date.timeIntervalSince(currentCode.validFrom))
|
||||
let progress = 1 - seconds / Double(key.period)
|
||||
return progress
|
||||
}
|
||||
}
|
||||
|
||||
struct KeyView_Previews: PreviewProvider {
|
||||
static var key: TOTPKey {
|
||||
TOTPKey(urlComponents: URLComponents(string: "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example")!)!
|
||||
}
|
||||
static var code: TOTPCode {
|
||||
OTPGenerator.generate(key: key)
|
||||
}
|
||||
static var previews: some View {
|
||||
KeyView(key: key, currentCode: code)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
//
|
||||
// KeysSection.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/24/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct KeysSection: View {
|
||||
@ObservedObject private var store: KeyStore = .shared
|
||||
@ObservedObject private var entryHolder: AppView.CodeHolder
|
||||
@State private var editedEntry: AppView.CodeEntry? = nil
|
||||
@State private var presentedQRCode: AppView.CodeEntry? = nil
|
||||
|
||||
init(codeHolder: AppView.CodeHolder) {
|
||||
self.entryHolder = codeHolder
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
ForEach(entryHolder.sortedEntries) { (entry) in
|
||||
KeyView(key: entry.key, currentCode: entry.code)
|
||||
// disabled because dropping onto list rows does not work :/
|
||||
// .onDrag {
|
||||
// NSItemProvider(object: entry.id.uuidString as NSString)
|
||||
// }
|
||||
.contextMenu {
|
||||
self.keyMenu(entry: entry)
|
||||
}
|
||||
}
|
||||
.onDelete { (indices) in
|
||||
withAnimation(.default) {
|
||||
for index in indices {
|
||||
store.removeKey(entryID: entryHolder.sortedEntries[index].id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: $editedEntry, content: self.editFormSheet)
|
||||
.sheet(item: $presentedQRCode, content: self.qrCodeSheet)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func keyMenu(entry: AppView.CodeEntry) -> some View {
|
||||
Section {
|
||||
Menu {
|
||||
Button("No Folder") {
|
||||
withAnimation {
|
||||
store.moveEntryToFolder(entryID: entry.id, folderID: nil)
|
||||
}
|
||||
}
|
||||
.disabled(entry.entry.folderID == nil)
|
||||
|
||||
Section {
|
||||
ForEach(store.sortedFolders) { (folder) in
|
||||
Button {
|
||||
withAnimation {
|
||||
store.moveEntryToFolder(entryID: entry.id, folderID: folder.id)
|
||||
}
|
||||
} label: {
|
||||
Text(folder.name)
|
||||
}
|
||||
.disabled(entry.entry.folderID == folder.id)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Move to Folder", systemImage: "folder")
|
||||
}
|
||||
|
||||
Button {
|
||||
// even the .contextMenu closure is called again, SwiftUI seems not to update the actual context menu buttons
|
||||
// so when this closure is called, it's copy of entry may be stale if the entry's since been edited
|
||||
// so we lookup the known current one by ID and edit that
|
||||
let realEntry = entryHolder.entries.first { $0.id == entry.id }
|
||||
editedEntry = realEntry
|
||||
} label: {
|
||||
Label("Edit Key", systemImage: "pencil")
|
||||
}
|
||||
|
||||
// todo: can't mark menu as destructive
|
||||
Menu {
|
||||
Button("Cancel", role: .cancel) {}
|
||||
|
||||
Button("Delete Key", role: .destructive) {
|
||||
// todo: why doesn't this animation work?
|
||||
withAnimation(.default) {
|
||||
store.removeKey(entryID: entry.id)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Delete Key", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
let realEntry = entryHolder.entries.first { $0.id == entry.id }
|
||||
presentedQRCode = realEntry
|
||||
} label: {
|
||||
Label("Export as QR", systemImage: "qrcode")
|
||||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.url = entry.key.url
|
||||
} label: {
|
||||
Label("Copy as URL", systemImage: "link")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func editFormSheet(editedEntry: AppView.CodeEntry) -> some View {
|
||||
NavigationView {
|
||||
EditKeyForm(editingKey: editedEntry.key) { (action) in
|
||||
self.editedEntry = nil
|
||||
switch action {
|
||||
case .cancel:
|
||||
break
|
||||
case .save(let key):
|
||||
store.updateKey(entryID: editedEntry.id, newKey: key)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Edit Key")
|
||||
}
|
||||
}
|
||||
|
||||
private func qrCodeSheet(entry: AppView.CodeEntry) -> some View {
|
||||
NavigationView {
|
||||
QRCodeView(key: entry.key)
|
||||
.id(entry.id)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct KeysSection_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
KeysSection(codeHolder: .init(store: .shared))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// PreferencesView.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/23/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PreferencesView: View {
|
||||
@ObservedObject private var store = KeyStore.shared
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var clearBeforeImport = false
|
||||
@State private var isPresentingExport = false
|
||||
@State private var isPresentingImportMode = false
|
||||
@State private var isPresentingImport = false
|
||||
@State private var isPresentingImportFailedAlert = false
|
||||
@State private var importFailedError: Error? = nil
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Backup") {
|
||||
Button("Export to File...") {
|
||||
isPresentingExport = true
|
||||
}
|
||||
Menu("Import from File...") {
|
||||
Button("Keep Existing Keys") {
|
||||
clearBeforeImport = false
|
||||
isPresentingImport = true
|
||||
}
|
||||
Button("Replace Existing Keys", role: .destructive) {
|
||||
clearBeforeImport = true
|
||||
isPresentingImport = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Preferences")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.fileExporter(isPresented: $isPresentingExport, document: BackupDocument(data: store.data), contentType: .propertyList, defaultFilename: "OTPBackup") { (_) in
|
||||
}
|
||||
.fileImporter(isPresented: $isPresentingImport, allowedContentTypes: [.propertyList], allowsMultipleSelection: false) { (result) in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
self.importFailedError = error
|
||||
self.isPresentingImportFailedAlert = true
|
||||
case let .success(urls):
|
||||
do {
|
||||
let backup = try BackupDocument(url: urls.first!)
|
||||
store.updateFromStore(backup.data, replaceExisting: clearBeforeImport)
|
||||
dismiss()
|
||||
} catch {
|
||||
self.importFailedError = error
|
||||
self.isPresentingImportFailedAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Import Failed", isPresented: $isPresentingImportFailedAlert) {
|
||||
Button("OK", role: .cancel) {}
|
||||
} message: {
|
||||
if let error = importFailedError {
|
||||
Text(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct PreferencesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PreferencesView()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
//
|
||||
// QRCodeView.swift
|
||||
// OTP
|
||||
//
|
||||
// Created by Shadowfacts on 8/23/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import OTPKit
|
||||
import CoreImage.CIFilterBuiltins
|
||||
|
||||
struct QRCodeView: View {
|
||||
let key: TOTPKey
|
||||
@State private var image: UIImage?
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var isPresentingShareSheet = false
|
||||
|
||||
init(key: TOTPKey) {
|
||||
self.key = key
|
||||
// self._image = State(initialValue: createImage())
|
||||
}
|
||||
|
||||
private func createImage() -> UIImage? {
|
||||
let filter = CIFilter.qrCodeGenerator()
|
||||
filter.message = key.url.absoluteString.data(using: .utf8)!
|
||||
let transform = CGAffineTransform(scaleX: 5, y: 5)
|
||||
|
||||
let context = CIContext()
|
||||
guard let output = filter.outputImage?.transformed(by: transform),
|
||||
let cgImage = context.createCGImage(output, from: output.extent) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let issuerFont = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .largeTitle).withSymbolicTraits(.traitBold)!, size: 0)
|
||||
let labelFont = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body), size: 0)
|
||||
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.lineBreakMode = .byWordWrapping
|
||||
|
||||
let issuer = key.issuer as NSString
|
||||
let size = CGSize(width: CGFloat(cgImage.width - 8), height: .greatestFiniteMagnitude)
|
||||
|
||||
var issuerRect = issuer.boundingRect(with: size, options: [.usesFontLeading, .usesLineFragmentOrigin], attributes: [.font: issuerFont, .paragraphStyle: paragraphStyle], context: nil)
|
||||
issuerRect.origin.y += 4
|
||||
issuerRect.origin.x += 4
|
||||
|
||||
var labelRect = CGRect.zero
|
||||
if let label = key.label, !label.isEmpty {
|
||||
labelRect = (label as NSString).boundingRect(with: size, options: [.usesFontLeading, .usesLineFragmentOrigin], attributes: [.font: labelFont, .paragraphStyle: paragraphStyle], context: nil)
|
||||
labelRect.origin.x += 4
|
||||
labelRect.origin.y += ceil(issuerRect.maxY)
|
||||
}
|
||||
|
||||
let imageSize = CGSize(width: CGFloat(cgImage.width), height: CGFloat(cgImage.height) + ceil(issuerRect.height) + ceil(labelRect.height) + 4)
|
||||
let renderer = UIGraphicsImageRenderer(size: imageSize)
|
||||
return renderer.image { (ctx) in
|
||||
ctx.cgContext.setFillColor(UIColor.white.cgColor)
|
||||
ctx.cgContext.fill(CGRect(origin: .zero, size: imageSize))
|
||||
|
||||
issuer.draw(in: issuerRect, withAttributes: [.font: issuerFont, .paragraphStyle: paragraphStyle])
|
||||
|
||||
if let label = key.label {
|
||||
(label as NSString).draw(in: labelRect, withAttributes: [.font: labelFont, .paragraphStyle: paragraphStyle])
|
||||
}
|
||||
|
||||
let imageRect = CGRect(x: 0, y: imageSize.height - CGFloat(cgImage.height), width: CGFloat(cgImage.width), height: CGFloat(cgImage.height))
|
||||
ctx.cgContext.draw(cgImage, in: imageRect)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
return mainView
|
||||
.navigationTitle("Export QR Code")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.background {
|
||||
ActivityView(isPresented: $isPresentingShareSheet, items: [image as Any, key.url])
|
||||
}
|
||||
.task {
|
||||
self.image = createImage()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var mainView: some View {
|
||||
if let image = image {
|
||||
VStack(alignment: .center) {
|
||||
Image(uiImage: image)
|
||||
.cornerRadius(5)
|
||||
.shadow(radius: 3)
|
||||
|
||||
HStack {
|
||||
Button {
|
||||
isPresentingShareSheet = true
|
||||
} label: {
|
||||
Label("Share", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
Spacer()
|
||||
Button(action: save) {
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
}
|
||||
}
|
||||
// fix the size so that the HStack doesn't grow beyond with width of the image
|
||||
.fixedSize()
|
||||
} else {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
}
|
||||
|
||||
private func save() {
|
||||
guard let image = image else { return }
|
||||
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct ActivityView: UIViewControllerRepresentable {
|
||||
var isPresented: Binding<Bool>
|
||||
let items: [Any]
|
||||
|
||||
func makeUIViewController(context: Context) -> Wrapper {
|
||||
return Wrapper(isPresented: isPresented, items: items)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: Wrapper, context: Context) {
|
||||
uiViewController.update(isPresented: isPresented, items: items)
|
||||
}
|
||||
|
||||
class Wrapper: UIViewController {
|
||||
private(set) var isPresented: Binding<Bool>!
|
||||
private(set) var items: [Any]!
|
||||
|
||||
init(isPresented: Binding<Bool>, items: [Any]) {
|
||||
self.isPresented = isPresented
|
||||
self.items = items
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(isPresented: Binding<Bool>, items: [Any]) {
|
||||
self.isPresented = isPresented
|
||||
self.items = items
|
||||
|
||||
if isPresented.wrappedValue, presentedViewController == nil {
|
||||
let vc = UIActivityViewController(activityItems: items, applicationActivities: nil)
|
||||
vc.completionWithItemsHandler = { (_, _, _, _) in
|
||||
isPresented.wrappedValue = false
|
||||
}
|
||||
present(vc, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct QRCodeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
QRCodeView(key: TOTPKey(urlComponents: URLComponents(string: "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example")!)!)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// OTPAlgorithm.swift
|
||||
// OTPKit
|
||||
//
|
||||
// Created by Shadowfacts on 8/21/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum OTPAlgorithm: Codable {
|
||||
case SHA1
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// OTPGenerator.swift
|
||||
// OTPKit
|
||||
//
|
||||
// Created by Shadowfacts on 8/21/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
public struct OTPGenerator {
|
||||
|
||||
private init() {}
|
||||
|
||||
public static func generate(key: OTPKey, counter: Int64) -> String {
|
||||
let data = withUnsafeBytes(of: counter.bigEndian) {
|
||||
Data($0)
|
||||
}
|
||||
|
||||
let mac = HMAC<Insecure.SHA1>.authenticationCode(for: data, using: SymmetricKey(data: key.secret))
|
||||
|
||||
let uint32Code = mac.withUnsafeBytes { (ptr) -> UInt32 in
|
||||
let offset = Int(ptr.last! & 0x0F)
|
||||
let offsetPtr = ptr.baseAddress!.advanced(by: offset)
|
||||
return offsetPtr.assumingMemoryBound(to: UInt32.self).pointee.bigEndian & 0x7FFFFFFF
|
||||
}
|
||||
|
||||
let truncated = uint32Code % UInt32(pow(10, Double(key.digits)))
|
||||
|
||||
var s = truncated.description
|
||||
if s.count < key.digits {
|
||||
s = String(repeating: "0", count: key.digits - s.count) + s
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
public static func generate(key: TOTPKey) -> TOTPCode {
|
||||
let now = Date()
|
||||
let counter = Int64(now.timeIntervalSince1970 / Double(key.period))
|
||||
let start = Date(timeIntervalSince1970: Double(counter * Int64(key.period)))
|
||||
let code = generate(key: key, counter: counter)
|
||||
return TOTPCode(code: code, validFrom: start, validInterval: key.period)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// OTPKey.swift
|
||||
// OTPKit
|
||||
//
|
||||
// Created by Shadowfacts on 8/21/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol OTPKey {
|
||||
var secret: Data { get }
|
||||
var digits: Int { get }
|
||||
var algorithm: OTPAlgorithm { get }
|
||||
var period: Int { get }
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
# ``OTPKit``
|
||||
|
||||
<!--@START_MENU_TOKEN@-->Summary<!--@END_MENU_TOKEN@-->
|
||||
|
||||
## Overview
|
||||
|
||||
<!--@START_MENU_TOKEN@-->Text<!--@END_MENU_TOKEN@-->
|
||||
|
||||
## Topics
|
||||
|
||||
### <!--@START_MENU_TOKEN@-->Group<!--@END_MENU_TOKEN@-->
|
||||
|
||||
- <!--@START_MENU_TOKEN@-->``Symbol``<!--@END_MENU_TOKEN@-->
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// OTPKit.h
|
||||
// OTPKit
|
||||
//
|
||||
// Created by Shadowfacts on 8/21/21.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for OTPKit.
|
||||
FOUNDATION_EXPORT double OTPKitVersionNumber;
|
||||
|
||||
//! Project version string for OTPKit.
|
||||
FOUNDATION_EXPORT const unsigned char OTPKitVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <OTPKit/PublicHeader.h>
|
||||
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// TOTPCode.swift
|
||||
// OTPKit
|
||||
//
|
||||
// Created by Shadowfacts on 8/21/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct TOTPCode: Equatable, Hashable {
|
||||
public let code: String
|
||||
public let validFrom: Date
|
||||
public let validInterval: Int // seconds
|
||||
public var validUntil: Date {
|
||||
validFrom.addingTimeInterval(TimeInterval(validInterval))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
//
|
||||
// TOTPKey.swift
|
||||
// OTPKit
|
||||
//
|
||||
// Created by Shadowfacts on 8/21/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct TOTPKey: OTPKey, Equatable, Hashable, Codable {
|
||||
public let secret: Data
|
||||
public let period: Int
|
||||
public let digits: Int
|
||||
public let algorithm: OTPAlgorithm
|
||||
public let label: String?
|
||||
public let issuer: String
|
||||
// let image: Data?
|
||||
|
||||
public init(secret: Data, period: Int, digits: Int, label: String?, issuer: String) {
|
||||
self.secret = secret
|
||||
self.period = period
|
||||
self.digits = digits
|
||||
self.algorithm = .SHA1
|
||||
self.label = label
|
||||
self.issuer = issuer
|
||||
}
|
||||
}
|
||||
|
||||
public extension TOTPKey {
|
||||
|
||||
init?(urlComponents: URLComponents) {
|
||||
guard urlComponents.scheme == "otpauth",
|
||||
urlComponents.host == "totp",
|
||||
let queryItems = urlComponents.queryItems else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var label: String?
|
||||
var issuer: String
|
||||
var secret: Data?
|
||||
var period: Int = 30
|
||||
var digits = 6
|
||||
|
||||
let path = urlComponents.path.drop(while: { $0 == "/" })
|
||||
let components = path.components(separatedBy: ":")
|
||||
if components.count > 1 {
|
||||
issuer = components[0]
|
||||
label = components[1]
|
||||
} else {
|
||||
issuer = components[0]
|
||||
}
|
||||
|
||||
for item in queryItems {
|
||||
guard let value = item.value else { continue }
|
||||
switch item.name.lowercased() {
|
||||
case "algorithm":
|
||||
if value.lowercased() != "sha1" {
|
||||
return nil
|
||||
}
|
||||
case "digits":
|
||||
if let newDigits = Int(value) {
|
||||
digits = newDigits
|
||||
}
|
||||
case "issuer":
|
||||
issuer = value
|
||||
case "period":
|
||||
if let newPeriod = Int(value) {
|
||||
period = newPeriod
|
||||
}
|
||||
case "secret":
|
||||
if let data = value.base32DecodedData {
|
||||
secret = data
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
guard let secret = secret else { return nil }
|
||||
self.issuer = issuer
|
||||
self.label = label
|
||||
self.secret = secret
|
||||
self.period = period
|
||||
self.digits = digits
|
||||
self.algorithm = .SHA1
|
||||
}
|
||||
|
||||
var url: URL {
|
||||
var components = URLComponents()
|
||||
|
||||
components.scheme = "otpauth"
|
||||
components.host = "totp"
|
||||
|
||||
if let label = label, !label.isEmpty {
|
||||
components.path = "/\(issuer):\(label)"
|
||||
} else {
|
||||
components.path = "/\(issuer)"
|
||||
}
|
||||
components.queryItems = [
|
||||
URLQueryItem(name: "algorithm", value: "SHA1"),
|
||||
URLQueryItem(name: "digits", value: digits.description),
|
||||
URLQueryItem(name: "issuer", value: issuer),
|
||||
URLQueryItem(name: "period", value: period.description),
|
||||
URLQueryItem(name: "secret", value: secret.base32EncodedString),
|
||||
]
|
||||
|
||||
return components.url!
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,399 @@
|
|||
//
|
||||
// Base32.swift
|
||||
// TOTP
|
||||
//
|
||||
// Created by 野村 憲男 on 1/24/15.
|
||||
//
|
||||
// Copyright (c) 2015 Norio Nomura
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Foundation
|
||||
|
||||
// https://tools.ietf.org/html/rfc4648
|
||||
|
||||
// MARK: - Base32 Data <-> String
|
||||
|
||||
public func base32Encode(_ data: Data) -> String {
|
||||
return data.withUnsafeBytes {
|
||||
base32encode($0.baseAddress!, $0.count, alphabetEncodeTable)
|
||||
}
|
||||
}
|
||||
|
||||
public func base32HexEncode(_ data: Data) -> String {
|
||||
return data.withUnsafeBytes {
|
||||
base32encode($0.baseAddress!, $0.count, extendedHexAlphabetEncodeTable)
|
||||
}
|
||||
}
|
||||
|
||||
public func base32DecodeToData(_ string: String) -> Data? {
|
||||
return base32decode(string, alphabetDecodeTable).flatMap(Data.init(_:))
|
||||
}
|
||||
|
||||
public func base32HexDecodeToData(_ string: String) -> Data? {
|
||||
return base32decode(string, extendedHexAlphabetDecodeTable).flatMap(Data.init(_:))
|
||||
}
|
||||
|
||||
// MARK: - Base32 [UInt8] <-> String
|
||||
|
||||
public func base32Encode(_ array: [UInt8]) -> String {
|
||||
return base32encode(array, array.count, alphabetEncodeTable)
|
||||
}
|
||||
|
||||
public func base32HexEncode(_ array: [UInt8]) -> String {
|
||||
return base32encode(array, array.count, extendedHexAlphabetEncodeTable)
|
||||
}
|
||||
|
||||
public func base32Decode(_ string: String) -> [UInt8]? {
|
||||
return base32decode(string, alphabetDecodeTable)
|
||||
}
|
||||
|
||||
public func base32HexDecode(_ string: String) -> [UInt8]? {
|
||||
return base32decode(string, extendedHexAlphabetDecodeTable)
|
||||
}
|
||||
|
||||
// MARK: extensions
|
||||
|
||||
extension String {
|
||||
// base32
|
||||
public var base32DecodedData: Data? {
|
||||
return base32DecodeToData(self)
|
||||
}
|
||||
|
||||
public var base32EncodedString: String {
|
||||
return utf8CString.withUnsafeBufferPointer {
|
||||
base32encode($0.baseAddress!, $0.count - 1, alphabetEncodeTable)
|
||||
}
|
||||
}
|
||||
|
||||
public func base32DecodedString(_ encoding: String.Encoding = .utf8) -> String? {
|
||||
return base32DecodedData.flatMap {
|
||||
String(data: $0, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
// base32Hex
|
||||
public var base32HexDecodedData: Data? {
|
||||
return base32HexDecodeToData(self)
|
||||
}
|
||||
|
||||
public var base32HexEncodedString: String {
|
||||
return utf8CString.withUnsafeBufferPointer {
|
||||
base32encode($0.baseAddress!, $0.count - 1, extendedHexAlphabetEncodeTable)
|
||||
}
|
||||
}
|
||||
|
||||
public func base32HexDecodedString(_ encoding: String.Encoding = .utf8) -> String? {
|
||||
return base32HexDecodedData.flatMap {
|
||||
String(data: $0, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
// base32
|
||||
public var base32EncodedString: String {
|
||||
return base32Encode(self)
|
||||
}
|
||||
|
||||
public var base32EncodedData: Data {
|
||||
return base32EncodedString.dataUsingUTF8StringEncoding
|
||||
}
|
||||
|
||||
public var base32DecodedData: Data? {
|
||||
return String(data: self, encoding: .utf8).flatMap(base32DecodeToData)
|
||||
}
|
||||
|
||||
// base32Hex
|
||||
public var base32HexEncodedString: String {
|
||||
return base32HexEncode(self)
|
||||
}
|
||||
|
||||
public var base32HexEncodedData: Data {
|
||||
return base32HexEncodedString.dataUsingUTF8StringEncoding
|
||||
}
|
||||
|
||||
public var base32HexDecodedData: Data? {
|
||||
return String(data: self, encoding: .utf8).flatMap(base32HexDecodeToData)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - private
|
||||
|
||||
private extension String {
|
||||
var dataUsingUTF8StringEncoding: Data {
|
||||
return utf8CString.withUnsafeBufferPointer {
|
||||
Data($0.dropLast().map(UInt8.init))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: encode
|
||||
|
||||
private let alphabetEncodeTable: [Int8] = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","2","3","4","5","6","7"].map { (c: UnicodeScalar) -> Int8 in Int8(c.value) }
|
||||
|
||||
private let extendedHexAlphabetEncodeTable: [Int8] = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V"].map { (c: UnicodeScalar) -> Int8 in Int8(c.value) }
|
||||
|
||||
private func base32encode(_ data: UnsafeRawPointer, _ length: Int, _ table: [Int8]) -> String {
|
||||
if length == 0 {
|
||||
return ""
|
||||
}
|
||||
var length = length
|
||||
|
||||
var bytes = data.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
let resultBufferSize = Int(ceil(Double(length) / 5)) * 8 + 1 // need null termination
|
||||
let resultBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: resultBufferSize)
|
||||
var encoded = resultBuffer
|
||||
|
||||
// encode regular blocks
|
||||
while length >= 5 {
|
||||
encoded[0] = table[Int(bytes[0] >> 3)]
|
||||
encoded[1] = table[Int((bytes[0] & 0b00000111) << 2 | bytes[1] >> 6)]
|
||||
encoded[2] = table[Int((bytes[1] & 0b00111110) >> 1)]
|
||||
encoded[3] = table[Int((bytes[1] & 0b00000001) << 4 | bytes[2] >> 4)]
|
||||
encoded[4] = table[Int((bytes[2] & 0b00001111) << 1 | bytes[3] >> 7)]
|
||||
encoded[5] = table[Int((bytes[3] & 0b01111100) >> 2)]
|
||||
encoded[6] = table[Int((bytes[3] & 0b00000011) << 3 | bytes[4] >> 5)]
|
||||
encoded[7] = table[Int((bytes[4] & 0b00011111))]
|
||||
length -= 5
|
||||
encoded = encoded.advanced(by: 8)
|
||||
bytes = bytes.advanced(by: 5)
|
||||
}
|
||||
|
||||
// encode last block
|
||||
var byte0, byte1, byte2, byte3, byte4: UInt8
|
||||
(byte0, byte1, byte2, byte3, byte4) = (0,0,0,0,0)
|
||||
switch length {
|
||||
case 4:
|
||||
byte3 = bytes[3]
|
||||
encoded[6] = table[Int((byte3 & 0b00000011) << 3 | byte4 >> 5)]
|
||||
encoded[5] = table[Int((byte3 & 0b01111100) >> 2)]
|
||||
fallthrough
|
||||
case 3:
|
||||
byte2 = bytes[2]
|
||||
encoded[4] = table[Int((byte2 & 0b00001111) << 1 | byte3 >> 7)]
|
||||
fallthrough
|
||||
case 2:
|
||||
byte1 = bytes[1]
|
||||
encoded[3] = table[Int((byte1 & 0b00000001) << 4 | byte2 >> 4)]
|
||||
encoded[2] = table[Int((byte1 & 0b00111110) >> 1)]
|
||||
fallthrough
|
||||
case 1:
|
||||
byte0 = bytes[0]
|
||||
encoded[1] = table[Int((byte0 & 0b00000111) << 2 | byte1 >> 6)]
|
||||
encoded[0] = table[Int(byte0 >> 3)]
|
||||
default: break
|
||||
}
|
||||
|
||||
// padding
|
||||
let pad = Int8(UnicodeScalar("=").value)
|
||||
switch length {
|
||||
case 0:
|
||||
encoded[0] = 0
|
||||
case 1:
|
||||
encoded[2] = pad
|
||||
encoded[3] = pad
|
||||
fallthrough
|
||||
case 2:
|
||||
encoded[4] = pad
|
||||
fallthrough
|
||||
case 3:
|
||||
encoded[5] = pad
|
||||
encoded[6] = pad
|
||||
fallthrough
|
||||
case 4:
|
||||
encoded[7] = pad
|
||||
fallthrough
|
||||
default:
|
||||
encoded[8] = 0
|
||||
break
|
||||
}
|
||||
|
||||
// return
|
||||
if let base32Encoded = String(validatingUTF8: resultBuffer) {
|
||||
resultBuffer.deallocate()
|
||||
return base32Encoded
|
||||
} else {
|
||||
resultBuffer.deallocate()
|
||||
fatalError("internal error")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: decode
|
||||
|
||||
private let __: UInt8 = 255
|
||||
private let alphabetDecodeTable: [UInt8] = [
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x00 - 0x0F
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x10 - 0x1F
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x20 - 0x2F
|
||||
__,__,26,27, 28,29,30,31, __,__,__,__, __,__,__,__, // 0x30 - 0x3F
|
||||
__, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, // 0x40 - 0x4F
|
||||
15,16,17,18, 19,20,21,22, 23,24,25,__, __,__,__,__, // 0x50 - 0x5F
|
||||
__, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, // 0x60 - 0x6F
|
||||
15,16,17,18, 19,20,21,22, 23,24,25,__, __,__,__,__, // 0x70 - 0x7F
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x80 - 0x8F
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x90 - 0x9F
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xA0 - 0xAF
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xB0 - 0xBF
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xC0 - 0xCF
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xD0 - 0xDF
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xE0 - 0xEF
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xF0 - 0xFF
|
||||
]
|
||||
|
||||
private let extendedHexAlphabetDecodeTable: [UInt8] = [
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x00 - 0x0F
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x10 - 0x1F
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x20 - 0x2F
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,__,__, __,__,__,__, // 0x30 - 0x3F
|
||||
__,10,11,12, 13,14,15,16, 17,18,19,20, 21,22,23,24, // 0x40 - 0x4F
|
||||
25,26,27,28, 29,30,31,__, __,__,__,__, __,__,__,__, // 0x50 - 0x5F
|
||||
__,10,11,12, 13,14,15,16, 17,18,19,20, 21,22,23,24, // 0x60 - 0x6F
|
||||
25,26,27,28, 29,30,31,__, __,__,__,__, __,__,__,__, // 0x70 - 0x7F
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x80 - 0x8F
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0x90 - 0x9F
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xA0 - 0xAF
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xB0 - 0xBF
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xC0 - 0xCF
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xD0 - 0xDF
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xE0 - 0xEF
|
||||
__,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__, // 0xF0 - 0xFF
|
||||
]
|
||||
|
||||
|
||||
private func base32decode(_ string: String, _ table: [UInt8]) -> [UInt8]? {
|
||||
let length = string.unicodeScalars.count
|
||||
if length == 0 {
|
||||
return []
|
||||
}
|
||||
|
||||
// calc padding length
|
||||
func getLeastPaddingLength(_ string: String) -> Int {
|
||||
if string.hasSuffix("======") {
|
||||
return 6
|
||||
} else if string.hasSuffix("====") {
|
||||
return 4
|
||||
} else if string.hasSuffix("===") {
|
||||
return 3
|
||||
} else if string.hasSuffix("=") {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// validate string
|
||||
let leastPaddingLength = getLeastPaddingLength(string)
|
||||
if let index = string.unicodeScalars.firstIndex(where: {$0.value > 0xff || table[Int($0.value)] > 31}) {
|
||||
// index points padding "=" or invalid character that table does not contain.
|
||||
let pos = string.unicodeScalars.distance(from: string.unicodeScalars.startIndex, to: index)
|
||||
// if pos points padding "=", it's valid.
|
||||
if pos != length - leastPaddingLength {
|
||||
print("string contains some invalid characters.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var remainEncodedLength = length - leastPaddingLength
|
||||
var additionalBytes = 0
|
||||
switch remainEncodedLength % 8 {
|
||||
// valid
|
||||
case 0: break
|
||||
case 2: additionalBytes = 1
|
||||
case 4: additionalBytes = 2
|
||||
case 5: additionalBytes = 3
|
||||
case 7: additionalBytes = 4
|
||||
default:
|
||||
print("string length is invalid.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// validated
|
||||
let dataSize = remainEncodedLength / 8 * 5 + additionalBytes
|
||||
|
||||
// Use UnsafePointer<UInt8>
|
||||
return string.utf8CString.withUnsafeBufferPointer {
|
||||
(data: UnsafeBufferPointer<CChar>) -> [UInt8] in
|
||||
var encoded = data.baseAddress!
|
||||
|
||||
var result = Array<UInt8>(repeating: 0, count: dataSize)
|
||||
var decodedOffset = 0
|
||||
|
||||
// decode regular blocks
|
||||
var value0, value1, value2, value3, value4, value5, value6, value7: UInt8
|
||||
(value0, value1, value2, value3, value4, value5, value6, value7) = (0,0,0,0,0,0,0,0)
|
||||
while remainEncodedLength >= 8 {
|
||||
value0 = table[Int(encoded[0])]
|
||||
value1 = table[Int(encoded[1])]
|
||||
value2 = table[Int(encoded[2])]
|
||||
value3 = table[Int(encoded[3])]
|
||||
value4 = table[Int(encoded[4])]
|
||||
value5 = table[Int(encoded[5])]
|
||||
value6 = table[Int(encoded[6])]
|
||||
value7 = table[Int(encoded[7])]
|
||||
|
||||
result[decodedOffset] = value0 << 3 | value1 >> 2
|
||||
result[decodedOffset + 1] = value1 << 6 | value2 << 1 | value3 >> 4
|
||||
result[decodedOffset + 2] = value3 << 4 | value4 >> 1
|
||||
result[decodedOffset + 3] = value4 << 7 | value5 << 2 | value6 >> 3
|
||||
result[decodedOffset + 4] = value6 << 5 | value7
|
||||
|
||||
remainEncodedLength -= 8
|
||||
decodedOffset += 5
|
||||
encoded = encoded.advanced(by: 8)
|
||||
}
|
||||
|
||||
// decode last block
|
||||
(value0, value1, value2, value3, value4, value5, value6, value7) = (0,0,0,0,0,0,0,0)
|
||||
switch remainEncodedLength {
|
||||
case 7:
|
||||
value6 = table[Int(encoded[6])]
|
||||
value5 = table[Int(encoded[5])]
|
||||
fallthrough
|
||||
case 5:
|
||||
value4 = table[Int(encoded[4])]
|
||||
fallthrough
|
||||
case 4:
|
||||
value3 = table[Int(encoded[3])]
|
||||
value2 = table[Int(encoded[2])]
|
||||
fallthrough
|
||||
case 2:
|
||||
value1 = table[Int(encoded[1])]
|
||||
value0 = table[Int(encoded[0])]
|
||||
default: break
|
||||
}
|
||||
switch remainEncodedLength {
|
||||
case 7:
|
||||
result[decodedOffset + 3] = value4 << 7 | value5 << 2 | value6 >> 3
|
||||
fallthrough
|
||||
case 5:
|
||||
result[decodedOffset + 2] = value3 << 4 | value4 >> 1
|
||||
fallthrough
|
||||
case 4:
|
||||
result[decodedOffset + 1] = value1 << 6 | value2 << 1 | value3 >> 4
|
||||
fallthrough
|
||||
case 2:
|
||||
result[decodedOffset] = value0 << 3 | value1 >> 2
|
||||
default: break
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// OTPGeneratorTests.swift
|
||||
// OTPKitTests
|
||||
//
|
||||
// Created by Shadowfacts on 8/21/21.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import OTPKit
|
||||
|
||||
class OTPGeneratorTests: XCTestCase {
|
||||
|
||||
func testGenerateTOTP() {
|
||||
let key = TOTPKey(urlComponents: URLComponents(string: "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example")!)!
|
||||
let result = OTPGenerator.generate(key: key, counter: 54319307)
|
||||
XCTAssertEqual(result, "155456")
|
||||
}
|
||||
|
||||
func testPadWithZeroes() {
|
||||
let key = TOTPKey(urlComponents: URLComponents(string: "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example")!)!
|
||||
let result = OTPGenerator.generate(key: key, counter: 54319378)
|
||||
XCTAssertEqual(result, "095891")
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// OTPKitTests.swift
|
||||
// OTPKitTests
|
||||
//
|
||||
// Created by Shadowfacts on 8/21/21.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import OTPKit
|
||||
|
||||
class OTPKitTests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
func testPerformanceExample() throws {
|
||||
// This is an example of a performance test case.
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
//
|
||||
// TOTPKeyTests.swift
|
||||
// OTPKitTests
|
||||
//
|
||||
// Created by Shadowfacts on 8/21/21.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import OTPKit
|
||||
|
||||
class TOTPKeyTests: XCTestCase {
|
||||
|
||||
func testDecodeSimpleKey() throws {
|
||||
let decoded = TOTPKey(urlComponents: URLComponents(string: "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example")!)
|
||||
XCTAssertNotNil(decoded)
|
||||
let key = decoded!
|
||||
XCTAssertEqual(key.secret, Data([
|
||||
UInt8(ascii: "H"),
|
||||
UInt8(ascii: "e"),
|
||||
UInt8(ascii: "l"),
|
||||
UInt8(ascii: "l"),
|
||||
UInt8(ascii: "o"),
|
||||
UInt8(ascii: "!"),
|
||||
0xDE, 0xAD, 0xBE, 0xEF
|
||||
]))
|
||||
XCTAssertEqual(key.issuer, "Example")
|
||||
XCTAssertEqual(key.label, "alice@google.com")
|
||||
}
|
||||
|
||||
func testDecodeOptionalParameters() throws {
|
||||
let decoded = TOTPKey(urlComponents: URLComponents(string: "otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=8&period=60")!)
|
||||
XCTAssertNotNil(decoded)
|
||||
let key = decoded!
|
||||
XCTAssertEqual(key.secret, Data([
|
||||
0x3D, 0xC6, 0xCA, 0xA4,
|
||||
0x82, 0x4A, 0x6D, 0x28,
|
||||
0x87, 0x67, 0xB2, 0x33,
|
||||
0x1E, 0x20, 0xB4, 0x31,
|
||||
0x66, 0xCB, 0x85, 0xD9,
|
||||
]))
|
||||
XCTAssertEqual(key.issuer, "ACME Co")
|
||||
XCTAssertEqual(key.label, "john.doe@email.com")
|
||||
XCTAssertEqual(key.period, 60)
|
||||
XCTAssertEqual(key.digits, 8)
|
||||
}
|
||||
|
||||
func testDecodeInvalidAlgorithm() {
|
||||
let components = URLComponents(string: "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256")!
|
||||
XCTAssertNil(TOTPKey(urlComponents: components))
|
||||
}
|
||||
|
||||
func testRoundtripURL() {
|
||||
let components = URLComponents(string: "otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=8&period=60")!
|
||||
let key = TOTPKey(urlComponents: components)
|
||||
XCTAssertNotNil(key)
|
||||
let outputComponents = URLComponents(url: key!.url, resolvingAgainstBaseURL: false)!
|
||||
XCTAssertEqual(outputComponents.scheme, components.scheme)
|
||||
XCTAssertEqual(outputComponents.host, components.host)
|
||||
XCTAssertEqual(outputComponents.path, components.path)
|
||||
XCTAssertEqual(
|
||||
outputComponents.queryItems!.sorted { (a, b) in a.name < b.name },
|
||||
components.queryItems!.sorted { (a, b) in a.name < b.name }
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue