Browse Source

Add the app

main
Shadowfacts 2 months ago
parent
commit
6d19c4b81d
  1. 78
      .gitignore
  2. 484
      OTP.xcodeproj/project.pbxproj
  3. 5
      OTP.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist
  4. 2
      OTP/AppDelegate.swift
  5. 45
      OTP/BackupDocument.swift
  6. 24
      OTP/Base.lproj/Main.storyboard
  7. 14
      OTP/DismissAction.swift
  8. 74
      OTP/EditedKey.swift
  9. 6
      OTP/Info.plist
  10. 121
      OTP/KeyData.swift
  11. 89
      OTP/KeyStore.swift
  12. 10
      OTP/SceneDelegate.swift
  13. 19
      OTP/ViewController.swift
  14. 95
      OTP/Views/AddQRView.swift
  15. 90
      OTP/Views/AddURLForm.swift
  16. 210
      OTP/Views/AppView.swift
  17. 39
      OTP/Views/CircularProgressView.swift
  18. 113
      OTP/Views/EditKeyForm.swift
  19. 80
      OTP/Views/FolderRow.swift
  20. 37
      OTP/Views/FolderView.swift
  21. 33
      OTP/Views/FoldersSection.swift
  22. 77
      OTP/Views/KeyView.swift
  23. 141
      OTP/Views/KeysSection.swift
  24. 79
      OTP/Views/PreferencesView.swift
  25. 169
      OTP/Views/QRCodeView.swift
  26. 12
      OTPKit/OTPAlgorithm.swift
  27. 45
      OTPKit/OTPGenerator.swift
  28. 15
      OTPKit/OTPKey.swift
  29. 13
      OTPKit/OTPKit.docc/OTPKit.md
  30. 18
      OTPKit/OTPKit.h
  31. 17
      OTPKit/TOTPCode.swift
  32. 110
      OTPKit/TOTPKey.swift
  33. 399
      OTPKit/Vendor/Base32.swift
  34. 25
      OTPKitTests/OTPGeneratorTests.swift
  35. 33
      OTPKitTests/OTPKitTests.swift
  36. 67
      OTPKitTests/TOTPKeyTests.swift

78
.gitignore

@ -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

484
OTP.xcodeproj/project.pbxproj

@ -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,16 +261,58 @@
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 = 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 */
/* Begin PBXProject 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
OTP.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist

@ -7,6 +7,11 @@
<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>
</dict>
</dict>

2
OTP/AppDelegate.swift

@ -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
}

45
OTP/BackupDocument.swift

@ -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)
}
}

24
OTP/Base.lproj/Main.storyboard

@ -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>

14
OTP/DismissAction.swift

@ -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)
}

74
OTP/EditedKey.swift

@ -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
}
}
}
}

6
OTP/Info.plist

@ -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>

121
OTP/KeyData.swift

@ -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
}
}

89
OTP/KeyStore.swift

@ -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)
}
}

10
OTP/SceneDelegate.swift

@ -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) {

19
OTP/ViewController.swift

@ -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.
}
}

95
OTP/Views/AddQRView.swift

@ -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 }
}
}

90
OTP/Views/AddURLForm.swift

@ -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 }
}
}

210
OTP/Views/AppView.swift

@ -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()
}
}

39
OTP/Views/CircularProgressView.swift

@ -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)