├── .github ├── FUNDING.yml └── workflows │ ├── deploying.yml │ └── testing.yml ├── .gitignore ├── .mailmap ├── .swift-version ├── .swiftformat ├── .swiftlint.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── _config.yml ├── fastlane ├── Appfile └── Fastfile ├── icon ├── icon.png ├── icon.sketch ├── icon_extension.png ├── icon_extension.sketch └── icon_round.png ├── img ├── app_store_badge.svg ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png └── screenshot4.png ├── pass.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ ├── WorkspaceSettings.xcsettings │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── pass.xcscheme │ ├── passAutoFillExtension.xcscheme │ ├── passExtension.xcscheme │ ├── passKit.xcscheme │ └── passShortcuts.xcscheme ├── pass ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-108@2x.png │ │ ├── Icon-20.png │ │ ├── Icon-20@2x-1.png │ │ ├── Icon-20@2x.png │ │ ├── Icon-20@3x.png │ │ ├── Icon-24@2x.png │ │ ├── Icon-27.5@2x.png │ │ ├── Icon-29.png │ │ ├── Icon-29@2x-1.png │ │ ├── Icon-29@2x-2.png │ │ ├── Icon-29@2x.png │ │ ├── Icon-29@3x-1.png │ │ ├── Icon-29@3x.png │ │ ├── Icon-30.png │ │ ├── Icon-40.png │ │ ├── Icon-40@2x-1.png │ │ ├── Icon-40@2x-2.png │ │ ├── Icon-40@2x.png │ │ ├── Icon-40@3x.png │ │ ├── Icon-44@2x.png │ │ ├── Icon-50.png │ │ ├── Icon-50@2x-1.png │ │ ├── Icon-50@2x.png │ │ ├── Icon-57.png │ │ ├── Icon-57@2x.png │ │ ├── Icon-60@2x.png │ │ ├── Icon-60@3x.png │ │ ├── Icon-72.png │ │ ├── Icon-72@2x.png │ │ ├── Icon-76.png │ │ ├── Icon-76@2x.png │ │ ├── Icon-83.5@2x.png │ │ ├── Icon-86@2x.png │ │ ├── Icon-98@2x.png │ │ ├── Icon-App-Store-iOS-1024@1x.png │ │ └── Icon-Apple-Watch-App-Store-1024@1x.png │ ├── AppIconBeta.appiconset │ │ ├── Contents.json │ │ ├── Icon-1024.png │ │ ├── Icon-1025.png │ │ ├── Icon-108@2x.png │ │ ├── Icon-20.png │ │ ├── Icon-20@2x-1.png │ │ ├── Icon-20@2x.png │ │ ├── Icon-20@3x.png │ │ ├── Icon-24@2x.png │ │ ├── Icon-27.5@2x.png │ │ ├── Icon-29.png │ │ ├── Icon-29@2x-1.png │ │ ├── Icon-29@2x-2.png │ │ ├── Icon-29@2x.png │ │ ├── Icon-29@3x-1.png │ │ ├── Icon-29@3x.png │ │ ├── Icon-40.png │ │ ├── Icon-40@2x-1.png │ │ ├── Icon-40@2x-2.png │ │ ├── Icon-40@2x.png │ │ ├── Icon-40@3x.png │ │ ├── Icon-44@2x.png │ │ ├── Icon-50@2x.png │ │ ├── Icon-60@2x.png │ │ ├── Icon-60@3x.png │ │ ├── Icon-76.png │ │ ├── Icon-76@2x.png │ │ ├── Icon-83.5@2x.png │ │ ├── Icon-86@2x.png │ │ └── Icon-98@2x.png │ ├── Camera.imageset │ │ ├── Contents.json │ │ ├── Screenshot-100.png │ │ └── Screenshot-50.png │ ├── Contents.json │ ├── Copy.imageset │ │ ├── Contents.json │ │ ├── Copy-100.png │ │ └── Copy-50.png │ ├── HorizontalSettings.imageset │ │ ├── Contents.json │ │ ├── Horizontal Settings Mixer-50.png │ │ └── Horizontal Settings Mixer-75.png │ ├── Invisible.imageset │ │ ├── Contents.json │ │ └── not-visible-interface-symbol-of-an-eye-with-a-slash-on-it.pdf │ ├── Lock.imageset │ │ ├── Contents.json │ │ ├── Password-50.png │ │ └── Password-75.png │ ├── PasswordImagePlaceHolder.imageset │ │ ├── Contents.json │ │ ├── id-card-dark.pdf │ │ ├── id-card-light.pdf │ │ └── id-card.pdf │ ├── Refresh.imageset │ │ ├── Contents.json │ │ ├── Refresh-64.png │ │ └── Refresh-75.png │ ├── Settings.imageset │ │ ├── Contents.json │ │ ├── Settings-50.png │ │ └── Settings-75.png │ └── Visible.imageset │ │ ├── Contents.json │ │ └── eye-visible-outlined-interface-symbol.pdf ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Controllers │ ├── AboutRepositoryTableViewController.swift │ ├── AboutTableViewController.swift │ ├── AddPasswordTableViewController.swift │ ├── AdvancedSettingsTableViewController.swift │ ├── AutoCellHeightUITableViewController.swift │ ├── BasicStaticTableViewController.swift │ ├── CommitLogsTableViewController.swift │ ├── EditPasswordTableViewController.swift │ ├── GeneralSettingsTableViewController.swift │ ├── GitConfigSettingsTableViewController.swift │ ├── GitRepositorySettingsTableViewController.swift │ ├── KeyImporter.swift │ ├── OpenSourceComponentsTableViewController.swift │ ├── PGPKeyArmorImportTableViewController.swift │ ├── PGPKeyFIleImportTableViewController.swift │ ├── PGPKeyImporter.swift │ ├── PGPKeyURLImportTableViewController.swift │ ├── PasswordDetailTableViewController.swift │ ├── PasswordEditorTableViewController.swift │ ├── PasswordNavigationViewController.swift │ ├── QRScannerController.swift │ ├── RawPasswordViewController.swift │ ├── SSHKeyArmorImportTableViewController.swift │ ├── SSHKeyFileImportTableViewController.swift │ ├── SSHKeyURLImportTableViewController.swift │ ├── SettingsSplitViewController.swift │ ├── SettingsTableViewController.swift │ └── SpecialThanksTableViewController.swift ├── Helpers │ ├── Objective-CBridgingHeader.h │ ├── PasswordAlertPresenter.swift │ ├── SecurePasteboard.swift │ └── UtilsExtension.swift ├── Info.plist ├── Models │ ├── QRKeyScanner.swift │ └── ScannableKeyType.swift ├── Services │ ├── PasswordDecryptor.swift │ ├── PasswordEncryptor.swift │ ├── PasswordManager.swift │ └── PasswordNavigationDataSource.swift ├── Views │ ├── FillPasswordTableViewCell.swift │ ├── FillPasswordTableViewCell.xib │ ├── LabelTableViewCell.swift │ ├── LabelTableViewCell.xib │ ├── PasswordDetailTitleTableViewCell.swift │ ├── PasswordDetailTitleTableViewCell.xib │ ├── PasswordGeneratorUISlider.swift │ ├── PasswordGeneratorUISwitch.swift │ ├── PasswordTableViewCell.swift │ ├── SliderTableViewCell.swift │ ├── SliderTableViewCell.xib │ ├── SwitchTableViewCell.swift │ ├── SwitchTableViewCell.xib │ ├── TextFieldTableViewCell.swift │ ├── TextFieldTableViewCell.xib │ ├── TextViewTableViewCell.swift │ ├── TextViewTableViewCell.xib │ ├── UICodeHighlightingLabel.swift │ └── UILocalizedLabel.swift ├── de.lproj │ ├── InfoPlist.strings │ ├── Intents.strings │ ├── Localizable.strings │ ├── Localizable.stringsdict │ └── Main.strings ├── en.lproj │ ├── InfoPlist.strings │ ├── Intents.intentdefinition │ ├── Localizable.strings │ ├── Localizable.stringsdict │ └── Main.strings ├── it.lproj │ ├── InfoPlist.strings │ ├── Intents.strings │ ├── Localizable.strings │ ├── Localizable.stringsdict │ └── Main.strings ├── pass.entitlements └── passBeta.entitlements ├── passAutoFillExtension ├── Base.lproj │ └── MainInterface.storyboard ├── Controllers │ ├── CredentialProviderViewController.swift │ ├── PasscodeExtensionDisplay.swift │ ├── PasscodeLockViewControllerForExtension.swift │ └── PasswordsViewController.swift ├── Info.plist ├── Protocols │ └── PasswordSelectionDelegate.swift ├── SearchPassword.storyboard ├── Services │ ├── CredentialProvider.swift │ └── PasswordsTableDataSource.swift ├── passAutoFillExtension.entitlements └── passBetaAutoFillExtension.entitlements ├── passExtension ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-57x57@1x.png │ │ ├── Icon-App-57x57@2x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-72x72@1x.png │ │ ├── Icon-App-72x72@2x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ ├── Icon-Small-50x50@1x.png │ │ └── Icon-Small-50x50@2x.png │ └── Contents.json ├── Base.lproj │ └── MainInterface.storyboard ├── Controllers │ └── ExtensionViewController.swift ├── Helpers │ └── ExtensionConstants.swift ├── Info.plist ├── Services │ └── CredentialProvider.swift ├── passBetaExtension.entitlements ├── passExtension.entitlements └── passProcessor.js ├── passKit ├── Assets.xcassets │ ├── Contents.json │ ├── PasscodeLockViewIcon.imageset │ │ ├── Contents.json │ │ ├── Icon-57.png │ │ ├── Icon-60@2x.png │ │ └── Icon-60@3x.png │ └── Wordlist │ │ ├── Contents.json │ │ ├── eff_long_wordlist.dataset │ │ ├── Contents.json │ │ └── eff_long_wordlist.txt │ │ └── eff_short_wordlist.dataset │ │ ├── Contents.json │ │ └── eff_short_wordlist.txt ├── Controllers │ ├── CoreDataStack.swift │ ├── PasscodeLockPresenter.swift │ └── PasscodeLockViewController.swift ├── Crypto │ ├── GopenPGPInterface.swift │ ├── ObjectivePGPInterface.swift │ ├── PGPAgent.swift │ └── PGPInterface.swift ├── Extensions │ ├── Array+Slices.swift │ ├── Data+Mutable.swift │ ├── String+Localization.swift │ ├── String+Utilities.swift │ ├── UIAlertActionExtension.swift │ ├── UIAlertControllerExtension.swift │ ├── UITextFieldExtension.swift │ ├── UIViewControllerExtension.swift │ ├── UIViewExtension.swift │ └── YKFSmartCardInterfaceExtension.swift ├── Helpers │ ├── AppError.swift │ ├── AppKeychain.swift │ ├── Colors.swift │ ├── CryptographicKeys.swift │ ├── DefaultsKeys.swift │ ├── FileManagerExtension.swift │ ├── Globals.swift │ ├── KeyFileManager.swift │ ├── KeyStore.swift │ ├── NotificationCenterDispatcher.swift │ ├── NotificationNames.swift │ ├── SearchBarScope.swift │ ├── Utils.swift │ ├── YubiKeyAPDU.swift │ └── YubiKeyConnection.swift ├── Info.plist ├── Models │ ├── GitCredential.swift │ ├── GitRepository.swift │ ├── PasscodeLock.swift │ ├── Password.swift │ ├── PasswordEntity.swift │ ├── PasswordStore.swift │ └── PasswordTableEntry.swift ├── Parser │ ├── AdditionField.swift │ ├── Constants.swift │ ├── OTPType.swift │ ├── Parser.swift │ ├── PasswordChange.swift │ └── TokenBuilder.swift ├── Passwords │ ├── PasswordGenerator.swift │ └── PasswordGeneratorFlavor.swift ├── Protocols │ └── AlertPresenting.swift ├── pass.xcdatamodeld │ └── pass.xcdatamodel │ │ └── contents └── passKit.h ├── passKitTests ├── CoreData │ ├── CoreDataTestCase.swift │ └── PasswordEntityTest.swift ├── Crypto │ ├── CryptoFrameworkTest.swift │ └── PGPAgentTest.swift ├── Extensions │ ├── Array+SlicesTest.swift │ └── String+UtilitiesTest.swift ├── Helpers │ └── KeyFileManagerTest.swift ├── Info.plist ├── Models │ ├── GitCredentialTest.swift │ ├── GitRepositoryTest.swift │ ├── PasswordStoreTest.swift │ ├── PasswordTableEntryTest.swift │ └── PasswordTest.swift ├── Parser │ ├── AdditionFieldTest.swift │ ├── ConstantsTest.swift │ ├── OTPTypeTest.swift │ ├── ParserTest.swift │ └── TokenBuilderTest.swift ├── Passwords │ ├── PasswordGeneratorFlavorTest.swift │ └── PasswordGeneratorTest.swift └── Testbase │ ├── DictBasedKeychain.swift │ ├── TestBase.swift │ └── TestPGPKeys.swift ├── passShortcuts ├── Info.plist ├── IntentHandler.swift ├── SyncRepositoryIntentHandler.swift ├── passBetaShortcuts.entitlements └── passShortcuts.entitlements ├── passTests ├── Info.plist └── Models │ ├── QRKeyScannerTest.swift │ └── ScannableKeyTypeTest.swift ├── patch └── gnu-dummy.patch └── scripts └── gopenpgp_build.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: mssun 2 | custom: "https://www.paypal.me/mssun" 3 | -------------------------------------------------------------------------------- /.github/workflows/deploying.yml: -------------------------------------------------------------------------------- 1 | name: Deploying 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - 'release-*' 8 | 9 | jobs: 10 | build: 11 | runs-on: macos-15 12 | strategy: 13 | matrix: 14 | channel: ['beta', 'release'] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: '3.3' 20 | bundler-cache: true 21 | - name: Setup Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: '1.23.x' 25 | - name: Installing packages 26 | run: | 27 | gem install bundler 28 | - uses: actions/cache@v4 29 | id: gopenpgp-cache 30 | with: 31 | path: go 32 | key: ${{ runner.os }}-gopenpgp-${{ secrets.CACHE_VERSION }}-${{ hashFiles('**/gopenpgp_build.sh') }} 33 | - name: Bundle Install 34 | run: | 35 | bundle config path vendor/bundle 36 | bundle install --jobs 4 --retry 3 37 | - name: GopenPGP 38 | if: ${{ steps.gopenpgp-cache.outputs.cache-hit == false }} 39 | run: | 40 | export PATH="/usr/local/opt/go/bin:$PATH" 41 | ./scripts/gopenpgp_build.sh 42 | - name: Test 43 | run: bundle exec fastlane test 44 | - name: Deploy 45 | run: bundle exec fastlane ${{ matrix.channel }} 46 | env: 47 | APPLE_ID: ${{ secrets.APPLE_ID }} 48 | APP_IDENTIFIER: ${{ secrets.APP_IDENTIFIER }} 49 | APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} 50 | APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} 51 | APP_STORE_CONNECT_API_KEY_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_CONTENT }} 52 | DESTINATION_EMAIL: ${{ secrets.DESTINATION_EMAIL }} 53 | EMAIL_FROM_NAME: ${{ secrets.EMAIL_FROM_NAME }} 54 | EMAIL_REPLY_TO: ${{ secrets.EMAIL_REPLY_TO }} 55 | FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }} 56 | FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} 57 | FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }} 58 | FASTLANE_USER: ${{ secrets.FASTLANE_USER }} 59 | ITC_TEAM_ID: ${{ secrets.ITC_TEAM_ID }} 60 | MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} 61 | MATCH_KEYCHAIN_NAME: ${{ secrets.MATCH_KEYCHAIN_NAME }} 62 | MATCH_KEYCHAIN_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }} 63 | MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} 64 | MY_API_KEY: ${{ secrets.MY_API_KEY }} 65 | MY_POSTMASTER: ${{ secrets.MY_POSTMASTER }} 66 | TEAM_ID: ${{ secrets.TEAM_ID }} 67 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | testing: 7 | runs-on: macos-15 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: ruby/setup-ruby@v1 11 | with: 12 | ruby-version: '3.3' 13 | bundler-cache: true 14 | - name: Setup Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: '1.23.x' 18 | - name: Installing packages 19 | run: | 20 | gem install bundler 21 | - uses: actions/cache@v4 22 | id: gopenpgp-cache 23 | with: 24 | path: go 25 | key: ${{ runner.os }}-gopenpgp-${{ secrets.CACHE_VERSION }}-${{ hashFiles('**/gopenpgp_build.sh') }} 26 | - name: Bundle Install 27 | run: | 28 | bundle config path vendor/bundle 29 | bundle install --jobs 4 --retry 3 30 | - name: GopenPGP 31 | if: ${{ steps.gopenpgp-cache.outputs.cache-hit == false }} 32 | run: ./scripts/gopenpgp_build.sh 33 | - name: Testing 34 | run: bundle exec fastlane test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | Pods/ 47 | Podfile.lock 48 | 49 | # Go Mobile Build results and dependency sources 50 | go/ 51 | 52 | # fastlane 53 | # 54 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 55 | # screenshots whenever they are needed. 56 | # For more information about the recommended setup visit: 57 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 58 | 59 | fastlane/README.md 60 | fastlane/report.xml 61 | fastlane/Preview.html 62 | fastlane/screenshots 63 | fastlane/test_output 64 | .DS_Store 65 | 66 | # Environment 67 | # 68 | # The Continuous Integration environment will create this file. It avoids specific "Run Script" phases while building. 69 | .ci-env 70 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Danny Moesch Danny Mösch 2 | 3 | Mingshen Sun Bob Sun 4 | 5 | Moritz Kuntze Moritz F. Kuntze 6 | 7 | Yishi Lin yishilin14 8 | 9 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.8 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # SwiftLint Configuration 2 | 3 | ## Folders to exclude from linting 4 | 5 | excluded: 6 | - go 7 | - Pods 8 | - vendor 9 | - xcode 10 | 11 | ## Active rules 12 | 13 | opt_in_rules: 14 | - all 15 | 16 | disabled_rules: 17 | - anonymous_argument_in_multiline_closure 18 | - balanced_xctest_lifecycle 19 | - contrasted_opening_brace 20 | - discouraged_none_name 21 | - discouraged_object_literal 22 | - discouraged_optional_collection # Too many false positives in implementations of system protocols. 23 | - empty_count # Too many false positives for variables named 'count'. 24 | - explicit_acl 25 | - explicit_enum_raw_value # Disabled in favor of 'redundant_string_enum_value'. 26 | - explicit_top_level_acl 27 | - explicit_type_interface 28 | - extension_access_modifier 29 | - file_header 30 | - file_length 31 | - file_name 32 | - file_types_order 33 | - force_cast 34 | - force_try 35 | - force_unwrapping 36 | - function_body_length 37 | - implicitly_unwrapped_optional 38 | - indentation_width 39 | - legacy_objc_type 40 | - line_length 41 | - missing_docs 42 | - no_empty_block # To be fixed later. 43 | - no_extension_access_modifier 44 | - no_grouping_extension 45 | - no_magic_numbers # Causes a lot of violations in tests. 46 | - number_separator # Contradicts with SwiftFormat rule 'decimalgrouping'. There are not many numbers anyway in the source code. 47 | - one_declaration_per_file 48 | - prefer_nimble 49 | - prefixed_toplevel_constant # Violations are mostly in test code. 50 | - private_outlet 51 | - prohibited_interface_builder 52 | - prohibited_super_call 53 | - required_deinit 54 | - self_binding 55 | - sorted_enum_cases # Wait for an auto-fix. 56 | - sorted_imports # Managed by SwiftFormat. 57 | - type_body_length 58 | - type_contents_order 59 | - unowned_variable_capture 60 | - vertical_whitespace_between_cases # Additional whitespace not needed because of visible indentation. 61 | 62 | ## Configuration for specific rules 63 | 64 | attributes: 65 | always_on_same_line: [] 66 | closure_body_length: 67 | warning: 40 68 | error: 60 69 | explicit_init: 70 | include_bare_init: true 71 | identifier_name: 72 | excluded: ["id", "to", "Defaults"] 73 | allowed_symbols: ["_"] 74 | multiline_arguments: 75 | only_enforce_after_first_closure_on_first_line: true 76 | type_name: 77 | max_length: 50 78 | trailing_closure: 79 | only_single_muted_parameter: true 80 | trailing_comma: 81 | mandatory_comma: true 82 | xct_specific_matcher: 83 | matchers: 84 | - two-argument-asserts 85 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | gem "xcodeproj" 5 | gem "rest-client" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Bob Sun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Pass 4 | [![GitHub release](https://img.shields.io/github/release/mssun/passforios.svg)](https://github.com/mssun/passforios/releases) 5 | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/passforios/passforios) 6 | [![Build Status](https://github.com/mssun/passforios/workflows/Deploying/badge.svg)](https://github.com/mssun/passforios/actions) 7 | [![Donate](https://img.shields.io/badge/paypal-donate-blue.svg)](https://www.paypal.me/mssun) 8 | 9 | Pass is an iOS client compatible with [ZX2C4's Pass command line application](http://www.passwordstore.org/). 10 | It is a password manager using GPG for encryption and Git for version control. 11 | 12 | Pass for iOS is available in App Store with the name "Pass - Password Store", and both iPhone and iPad are supported. 13 | 14 |

15 | Download on the App Store 16 |

17 | 18 | You can also help us test beta versions through [TestFlight](https://testflight.apple.com/join/whK4zUFG). 19 | 20 | ## Features 21 | 22 | - Compatible with the Password Store command line tool. 23 | - View, copy, add, and edit password entries. 24 | - Encrypt and decrypt password entries by PGP keys. 25 | - Synchronize with your password Git repository. 26 | - User-friendly interface: search, long press to copy, copy and open link, etc. 27 | - Support one-time password tokens (two-factor authentication codes). 28 | - AutoFill in Safari/Chrome and [supported apps](https://github.com/agilebits/onepassword-app-extension). 29 | - Support YubiKey. 30 | 31 | ## Screenshots 32 | 33 |

34 | 35 | 36 | 37 | 38 |

39 | 40 | ## Usages 41 | 42 | - Setup your password-store ([official `Pass` introduction](https://www.passwordstore.org/)) 43 | - Get Pass for iOS from the App Store or [build by yourself](https://github.com/mssun/passforios/wiki/Building-Pass-for-iOS) 44 | - Setup Pass for iOS ([quick-start guide](https://github.com/mssun/passforios/wiki#quick-start-guide-for-pass-for-ios)) 45 | 46 | For more, please read the [wiki page](https://github.com/mssun/passforios/wiki). 47 | 48 | ## Building Pass for iOS 49 | 50 | 1. Install Go: `brew install go`. 51 | 1. Run `./scripts/gopenpgp_build.sh` to build GopenPGP. 52 | 1. Open the `pass.xcodeproj` file in Xcode. 53 | 1. Build & Run. 54 | 55 | ## License 56 | 57 | MIT 58 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal 2 | 3 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier ENV["APP_IDENTIFIER"] 2 | apple_id ENV["APPLE_ID"] 3 | team_id ENV["TEAM_ID"] 4 | itc_team_id ENV["ITC_TEAM_ID"] 5 | -------------------------------------------------------------------------------- /icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/icon/icon.png -------------------------------------------------------------------------------- /icon/icon.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/icon/icon.sketch -------------------------------------------------------------------------------- /icon/icon_extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/icon/icon_extension.png -------------------------------------------------------------------------------- /icon/icon_extension.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/icon/icon_extension.sketch -------------------------------------------------------------------------------- /icon/icon_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/icon/icon_round.png -------------------------------------------------------------------------------- /img/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/img/screenshot1.png -------------------------------------------------------------------------------- /img/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/img/screenshot2.png -------------------------------------------------------------------------------- /img/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/img/screenshot3.png -------------------------------------------------------------------------------- /img/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/img/screenshot4.png -------------------------------------------------------------------------------- /pass.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pass.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /pass.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /pass.xcodeproj/xcshareddata/xcschemes/passKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-108@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-108@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-20.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-20@2x-1.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-24@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-27.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-27.5@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-29.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-2.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-29@3x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-29@3x-1.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-30.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-40.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-2.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-44@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-44@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-50.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-50@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-50@2x-1.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-50@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-57.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-57@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-72.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-86@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-86@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-98@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-98@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-App-Store-iOS-1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-App-Store-iOS-1024@1x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIcon.appiconset/Icon-Apple-Watch-App-Store-1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIcon.appiconset/Icon-Apple-Watch-App-Store-1024@1x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-1024.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-1025.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-1025.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-108@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-108@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-20.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-20@2x-1.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-20@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-20@3x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-24@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-27.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-27.5@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-29.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-29@2x-1.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-29@2x-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-29@2x-2.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-29@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-29@3x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-29@3x-1.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-29@3x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-40.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-40@2x-1.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-40@2x-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-40@2x-2.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-40@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-40@3x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-44@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-44@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-50@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-76.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-83.5@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-86@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-86@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/AppIconBeta.appiconset/Icon-98@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/AppIconBeta.appiconset/Icon-98@2x.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/Camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "Screenshot-50.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "Screenshot-100.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /pass/Assets.xcassets/Camera.imageset/Screenshot-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/Camera.imageset/Screenshot-100.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/Camera.imageset/Screenshot-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/Camera.imageset/Screenshot-50.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /pass/Assets.xcassets/Copy.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "Copy-50.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "Copy-100.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /pass/Assets.xcassets/Copy.imageset/Copy-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/Copy.imageset/Copy-100.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/Copy.imageset/Copy-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/Copy.imageset/Copy-50.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/HorizontalSettings.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "Horizontal Settings Mixer-50.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "Horizontal Settings Mixer-75.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /pass/Assets.xcassets/HorizontalSettings.imageset/Horizontal Settings Mixer-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/HorizontalSettings.imageset/Horizontal Settings Mixer-50.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/HorizontalSettings.imageset/Horizontal Settings Mixer-75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/HorizontalSettings.imageset/Horizontal Settings Mixer-75.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/Invisible.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "not-visible-interface-symbol-of-an-eye-with-a-slash-on-it.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /pass/Assets.xcassets/Invisible.imageset/not-visible-interface-symbol-of-an-eye-with-a-slash-on-it.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/Invisible.imageset/not-visible-interface-symbol-of-an-eye-with-a-slash-on-it.pdf -------------------------------------------------------------------------------- /pass/Assets.xcassets/Lock.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "Password-50.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "Password-75.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /pass/Assets.xcassets/Lock.imageset/Password-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/Lock.imageset/Password-50.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/Lock.imageset/Password-75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/Lock.imageset/Password-75.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/PasswordImagePlaceHolder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "id-card.pdf" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "id-card-light.pdf", 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "light" 14 | } 15 | ] 16 | }, 17 | { 18 | "idiom" : "universal", 19 | "filename" : "id-card-dark.pdf", 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "dark" 24 | } 25 | ] 26 | } 27 | ], 28 | "info" : { 29 | "version" : 1, 30 | "author" : "xcode" 31 | } 32 | } -------------------------------------------------------------------------------- /pass/Assets.xcassets/PasswordImagePlaceHolder.imageset/id-card-dark.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/PasswordImagePlaceHolder.imageset/id-card-dark.pdf -------------------------------------------------------------------------------- /pass/Assets.xcassets/PasswordImagePlaceHolder.imageset/id-card-light.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/PasswordImagePlaceHolder.imageset/id-card-light.pdf -------------------------------------------------------------------------------- /pass/Assets.xcassets/PasswordImagePlaceHolder.imageset/id-card.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/PasswordImagePlaceHolder.imageset/id-card.pdf -------------------------------------------------------------------------------- /pass/Assets.xcassets/Refresh.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "Refresh-64.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "Refresh-75.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /pass/Assets.xcassets/Refresh.imageset/Refresh-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/Refresh.imageset/Refresh-64.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/Refresh.imageset/Refresh-75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/Refresh.imageset/Refresh-75.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/Settings.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "Settings-50.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "Settings-75.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /pass/Assets.xcassets/Settings.imageset/Settings-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/Settings.imageset/Settings-50.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/Settings.imageset/Settings-75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/Settings.imageset/Settings-75.png -------------------------------------------------------------------------------- /pass/Assets.xcassets/Visible.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "eye-visible-outlined-interface-symbol.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /pass/Assets.xcassets/Visible.imageset/eye-visible-outlined-interface-symbol.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/Assets.xcassets/Visible.imageset/eye-visible-outlined-interface-symbol.pdf -------------------------------------------------------------------------------- /pass/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /pass/Controllers/AboutTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutTableViewController.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 8/2/2017. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AboutTableViewController: BasicStaticTableViewController { 12 | override func viewDidLoad() { 13 | tableData = [ 14 | // section 0 15 | [ 16 | [.title: "Website".localize(), .action: "link", .link: "https://github.com/mssun/pass-ios.git"], 17 | [.title: "Help".localize(), .action: "link", .link: "https://github.com/mssun/passforios/wiki"], 18 | [.title: "ContactDeveloper".localize(), .action: "link", .link: "mailto:developer@passforios.mssun.me?subject=Pass%20for%20iOS"], 19 | ], 20 | 21 | // section 1, 22 | [ 23 | [.title: "OpenSourceComponents".localize(), .action: "segue", .link: "showOpenSourceComponentsSegue"], 24 | [.title: "SpecialThanks".localize(), .action: "segue", .link: "showSpecialThanksSegue"], 25 | ], 26 | ] 27 | super.viewDidLoad() 28 | } 29 | 30 | override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { 31 | if section == tableData.count - 1 { 32 | let view = UIView() 33 | let footerLabel = UILabel(frame: CGRect(x: -16, y: 16, width: tableView.frame.width, height: 60)) 34 | footerLabel.numberOfLines = 0 35 | footerLabel.text = "PassForIos".localize() + " \(Bundle.main.releaseVersionNumber!) (\(Bundle.main.buildVersionNumber!))" 36 | footerLabel.font = UIFont.preferredFont(forTextStyle: .footnote) 37 | footerLabel.textColor = UIColor.lightGray 38 | footerLabel.textAlignment = .center 39 | view.addSubview(footerLabel) 40 | return view 41 | } 42 | return nil 43 | } 44 | 45 | override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? { 46 | if section == 1 { 47 | return "Acknowledgements".localize().uppercased() 48 | } 49 | return nil 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pass/Controllers/AddPasswordTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddPasswordTableViewController.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 10/2/2017. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | import UIKit 11 | 12 | class AddPasswordTableViewController: PasswordEditorTableViewController { 13 | var defaultDirPrefix = "" 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | tableData[0][0][PasswordEditorCellKey.content] = defaultDirPrefix 18 | } 19 | 20 | override func shouldPerformSegue(withIdentifier identifier: String, sender _: Any?) -> Bool { 21 | if identifier == "saveAddPasswordSegue" { 22 | // check PGP key 23 | guard PGPAgent.shared.isPrepared else { 24 | let alertTitle = "CannotAddPassword".localize() 25 | let alertMessage = "PgpKeyNotSet.".localize() 26 | Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil) 27 | return false 28 | } 29 | 30 | // check name 31 | guard checkName() else { 32 | return false 33 | } 34 | } 35 | return true 36 | } 37 | 38 | @IBAction 39 | private func cancel(_: Any) { 40 | navigationController?.popViewController(animated: true) 41 | } 42 | 43 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 44 | super.prepare(for: segue, sender: sender) 45 | if segue.identifier == "saveAddPasswordSegue" { 46 | let (name, path) = getNamePath() 47 | password = Password(name: name, path: path, plainText: plainText) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pass/Controllers/AutoCellHeightUITableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoCellHeightUITableViewController.swift 3 | // pass 4 | // 5 | // Created by Danny Moesch on 17.02.19. 6 | // Copyright © 2019 Bob Sun. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AutoCellHeightUITableViewController: UITableViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | tableView.estimatedRowHeight = 170 15 | tableView.rowHeight = UITableView.automaticDimension 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pass/Controllers/CommitLogsTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommitLogsTableViewController.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 28/2/2017. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import ObjectiveGit 10 | import passKit 11 | import UIKit 12 | 13 | class CommitLogsTableViewController: UITableViewController { 14 | var commits: [GTCommit] = [] 15 | let passwordStore = PasswordStore.shared 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | NotificationCenter.default.addObserver(self, selector: #selector(updateCommitLogs), name: .passwordStoreUpdated, object: nil) 20 | commits = getCommitLogs() 21 | tableView.estimatedRowHeight = 50 22 | tableView.rowHeight = UITableView.automaticDimension 23 | } 24 | 25 | override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { 26 | commits.count 27 | } 28 | 29 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 30 | let cell = tableView.dequeueReusableCell(withIdentifier: "commitLogCell", for: indexPath) 31 | let formatter = DateFormatter() 32 | formatter.dateStyle = DateFormatter.Style.medium 33 | formatter.timeStyle = .medium 34 | let dateString = formatter.string(from: commits[indexPath.row].commitDate) 35 | 36 | let author = cell.contentView.viewWithTag(200) as? UILabel 37 | let dateLabel = cell.contentView.viewWithTag(201) as? UILabel 38 | let messageLabel = cell.contentView.viewWithTag(202) as? UILabel 39 | author?.text = commits[indexPath.row].author?.name 40 | dateLabel?.text = dateString 41 | messageLabel?.text = commits[indexPath.row].message?.trimmed 42 | return cell 43 | } 44 | 45 | @objc 46 | func updateCommitLogs() { 47 | commits = getCommitLogs() 48 | tableView.reloadData() 49 | } 50 | 51 | private func getCommitLogs() -> [GTCommit] { 52 | do { 53 | return try passwordStore.getRecentCommits(count: 20) 54 | } catch { 55 | Utils.alert(title: "Error".localize(), message: error.localizedDescription, controller: self, completion: nil) 56 | return [] 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pass/Controllers/EditPasswordTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditPasswordTableViewController.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 12/2/2017. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | import UIKit 11 | 12 | class EditPasswordTableViewController: PasswordEditorTableViewController { 13 | override func shouldPerformSegue(withIdentifier identifier: String, sender _: Any?) -> Bool { 14 | if identifier == "saveEditPasswordSegue" { 15 | // check name 16 | guard checkName() else { 17 | return false 18 | } 19 | } 20 | return true 21 | } 22 | 23 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 24 | super.prepare(for: segue, sender: sender) 25 | if segue.identifier == "saveEditPasswordSegue" { 26 | let editedPlainText = plainText 27 | let (name, path) = getNamePath() 28 | if password!.plainText != editedPlainText || password!.path != path { 29 | password!.updatePassword(name: name, path: path, plainText: editedPlainText) 30 | } 31 | if let controller = segue.destination as? PasswordDetailTableViewController { 32 | controller.password = password 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pass/Controllers/GitConfigSettingsTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitConfigSettingsTableViewController.swift 3 | // pass 4 | // 5 | // Created by Yishi Lin on 10/4/17. 6 | // Copyright © 2017 Yishi Lin. All rights reserved. 7 | // 8 | 9 | import passKit 10 | import UIKit 11 | 12 | class GitConfigSettingsTableViewController: UITableViewController { 13 | let passwordStore = PasswordStore.shared 14 | 15 | @IBOutlet var nameTextField: UITextField! 16 | @IBOutlet var emailTextField: UITextField! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | tableView.rowHeight = UITableView.automaticDimension 21 | 22 | let signature = passwordStore.gitSignatureForNow 23 | nameTextField.placeholder = signature?.name ?? "" 24 | emailTextField.placeholder = signature?.email ?? "" 25 | nameTextField.text = Defaults.gitSignatureName 26 | emailTextField.text = Defaults.gitSignatureEmail 27 | } 28 | 29 | override func shouldPerformSegue(withIdentifier identifier: String, sender _: Any?) -> Bool { 30 | if identifier == "saveGitConfigSettingSegue" { 31 | let name = nameTextField.text!.isEmpty ? Globals.gitSignatureDefaultName : nameTextField.text! 32 | let email = emailTextField.text!.isEmpty ? Globals.gitSignatureDefaultEmail : nameTextField.text! 33 | guard GTSignature(name: name, email: email, time: nil) != nil else { 34 | Utils.alert(title: "Error".localize(), message: "InvalidNameOrEmail".localize(), controller: self, completion: nil) 35 | return false 36 | } 37 | } 38 | return true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pass/Controllers/KeyImporter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyImporter.swift 3 | // pass 4 | // 5 | // Created by Danny Moesch on 15.02.20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | 11 | protocol KeyImporter { 12 | static var keySource: KeySource { get } 13 | 14 | static var label: String { get } 15 | 16 | static var isCurrentKeySource: Bool { get } 17 | 18 | static var menuLabel: String { get } 19 | 20 | func isReadyToUse() -> Bool 21 | 22 | func importKeys() throws 23 | } 24 | 25 | extension KeyImporter { 26 | static var isCurrentKeySource: Bool { 27 | Defaults.gitSSHKeySource == keySource 28 | } 29 | 30 | static var menuLabel: String { 31 | if isCurrentKeySource { 32 | return "✓ \(label)" 33 | } 34 | return label 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pass/Controllers/PGPKeyImporter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PGPKeyImporter.swift 3 | // pass 4 | // 5 | // Created by Danny Moesch on 07.02.20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | 11 | protocol PGPKeyImporter: KeyImporter { 12 | func doAfterImport() 13 | } 14 | 15 | extension PGPKeyImporter { 16 | static var isCurrentKeySource: Bool { 17 | Defaults.pgpKeySource == keySource 18 | } 19 | 20 | func doAfterImport() {} 21 | } 22 | -------------------------------------------------------------------------------- /pass/Controllers/PGPKeyURLImportTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PGPKeyURLImportTableViewController.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 21/1/2017. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | import UIKit 11 | 12 | class PGPKeyURLImportTableViewController: AutoCellHeightUITableViewController, AlertPresenting { 13 | @IBOutlet var pgpPublicKeyURLTextField: UITextField! 14 | @IBOutlet var pgpPrivateKeyURLTextField: UITextField! 15 | 16 | var pgpPrivateKeyURL: URL? 17 | var pgpPublicKeyURL: URL? 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | pgpPublicKeyURLTextField.text = Defaults.pgpPublicKeyURL?.absoluteString 22 | pgpPrivateKeyURLTextField.text = Defaults.pgpPrivateKeyURL?.absoluteString 23 | } 24 | 25 | @IBAction 26 | private func save(_: Any) { 27 | pgpPublicKeyURL = validate(pgpKeyURLText: pgpPublicKeyURLTextField.text) 28 | 29 | if !Defaults.isYubiKeyEnabled { 30 | pgpPrivateKeyURL = validate(pgpKeyURLText: pgpPrivateKeyURLTextField.text) 31 | } 32 | saveImportedKeys() 33 | } 34 | } 35 | 36 | extension PGPKeyURLImportTableViewController: PGPKeyImporter { 37 | static let keySource = KeySource.url 38 | static let label = "DownloadFromUrl".localize() 39 | 40 | func isReadyToUse() -> Bool { 41 | validate(pgpKeyURLText: pgpPublicKeyURLTextField.text) != nil 42 | && (Defaults.isYubiKeyEnabled || validate(pgpKeyURLText: pgpPrivateKeyURLTextField.text ?? "") != nil) 43 | } 44 | 45 | func importKeys() throws { 46 | if let pgpPrivateKeyURL { 47 | Defaults.pgpPrivateKeyURL = pgpPrivateKeyURL 48 | try KeyFileManager.PrivatePGP.importKey(from: pgpPrivateKeyURL) 49 | } 50 | 51 | if let pgpPublicKeyURL { 52 | Defaults.pgpPublicKeyURL = pgpPublicKeyURL 53 | try KeyFileManager.PublicPGP.importKey(from: pgpPublicKeyURL) 54 | } 55 | } 56 | 57 | func doAfterImport() { 58 | presentAlert(title: "RememberToRemoveKey".localize(), message: "RememberToRemoveKeyFromServer.".localize()) 59 | } 60 | 61 | func saveImportedKeys() { 62 | performSegue(withIdentifier: "savePGPKeySegue", sender: self) 63 | } 64 | 65 | private func validate(pgpKeyURLText: String?) -> URL? { 66 | guard let pgpKeyURL = pgpKeyURLText, let url = URL(string: pgpKeyURL) else { 67 | presentFailureAlert(title: "CannotSavePgpKey".localize(), message: "SetPgpKeyUrlsFirst.".localize()) 68 | return nil 69 | } 70 | guard url.scheme == "https" || url.scheme == "http" else { 71 | presentFailureAlert(title: "CannotSavePgpKey".localize(), message: "UseEitherHttpsOrHttp.".localize()) 72 | return nil 73 | } 74 | return url 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /pass/Controllers/RawPasswordViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawPasswordViewController.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 31/3/2017. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | import UIKit 11 | 12 | class RawPasswordViewController: UIViewController { 13 | @IBOutlet var rawPasswordTextView: UITextView! 14 | var password: Password? 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | navigationItem.title = password?.name 19 | rawPasswordTextView.textContainer.lineFragmentPadding = 0 20 | rawPasswordTextView.textContainerInset = .zero 21 | rawPasswordTextView.text = password?.plainText 22 | rawPasswordTextView.textColor = Colors.label 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pass/Controllers/SSHKeyFileImportTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSHKeyFileImportTableViewController.swift 3 | // pass 4 | // 5 | // Created by Danny Moesch on 15.02.20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | import SVProgressHUD 11 | 12 | class SSHKeyFileImportTableViewController: AutoCellHeightUITableViewController { 13 | @IBOutlet var sshPrivateKeyFile: UITableViewCell! 14 | 15 | private var privateKey: String? 16 | 17 | @IBAction 18 | private func doneButtonTapped(_: Any) { 19 | performSegue(withIdentifier: "importSSHKeySegue", sender: self) 20 | } 21 | 22 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 23 | let cell = tableView.cellForRow(at: indexPath) 24 | let picker = UIDocumentPickerViewController(documentTypes: ["public.data"], in: .open) 25 | cell?.isSelected = false 26 | guard cell == sshPrivateKeyFile else { 27 | return 28 | } 29 | picker.delegate = self 30 | picker.shouldShowFileExtensions = true 31 | present(picker, animated: true, completion: nil) 32 | } 33 | } 34 | 35 | extension SSHKeyFileImportTableViewController: UIDocumentPickerDelegate { 36 | func documentPicker(_: UIDocumentPickerViewController, didPickDocumentsAt url: [URL]) { 37 | guard let url = url.first else { 38 | return 39 | } 40 | let fileName = url.lastPathComponent 41 | do { 42 | // Start accessing a security-scoped resource. 43 | guard url.startAccessingSecurityScopedResource() else { 44 | // Handle the failure here. 45 | throw AppError.readingFile(fileName: fileName) 46 | } 47 | 48 | // Make sure you release the security-scoped resource when you are done. 49 | defer { url.stopAccessingSecurityScopedResource() } 50 | 51 | privateKey = try String(contentsOf: url, encoding: .ascii) 52 | sshPrivateKeyFile.textLabel?.text = fileName 53 | } catch { 54 | let message = "FileCannotBeImported.".localize(fileName) | "UnderlyingError".localize(error.localizedDescription) 55 | Utils.alert(title: "CannotImportFile".localize(), message: message, controller: self) 56 | } 57 | } 58 | } 59 | 60 | extension SSHKeyFileImportTableViewController: KeyImporter { 61 | static let keySource = KeySource.file 62 | static let label = "LoadFromFiles".localize() 63 | 64 | func isReadyToUse() -> Bool { 65 | guard privateKey != nil else { 66 | Utils.alert(title: "CannotSave".localize(), message: "SetPrivateKeyUrl.".localize(), controller: self) 67 | return false 68 | } 69 | return true 70 | } 71 | 72 | func importKeys() throws { 73 | guard let privateKey else { 74 | return 75 | } 76 | try KeyFileManager.PrivateSSH.importKey(from: privateKey) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pass/Controllers/SSHKeyURLImportTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSHKeyURLImportTableViewController.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 25/1/2017. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | import SVProgressHUD 11 | 12 | class SSHKeyURLImportTableViewController: AutoCellHeightUITableViewController { 13 | @IBOutlet var privateKeyURLTextField: UITextField! 14 | 15 | var sshPrivateKeyURL: URL? 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | privateKeyURLTextField.text = Defaults.gitSSHPrivateKeyURL?.absoluteString 20 | } 21 | 22 | @IBAction 23 | private func doneButtonTapped(_: UIButton) { 24 | guard let text = privateKeyURLTextField.text, 25 | let privateKeyURL = URL(string: text) else { 26 | Utils.alert(title: "CannotSave".localize(), message: "SetPrivateKeyUrl.".localize(), controller: self) 27 | return 28 | } 29 | 30 | if privateKeyURL.scheme?.lowercased() == "http" { 31 | let savePassphraseAlert = UIAlertController(title: "HttpNotSecure".localize(), message: "ReallyUseHttp?".localize(), preferredStyle: .alert) 32 | savePassphraseAlert.addAction(UIAlertAction(title: "No".localize(), style: .default) { _ in }) 33 | savePassphraseAlert.addAction( 34 | UIAlertAction(title: "Yes".localize(), style: .destructive) { _ in 35 | self.sshPrivateKeyURL = privateKeyURL 36 | self.performSegue(withIdentifier: "importSSHKeySegue", sender: self) 37 | } 38 | ) 39 | present(savePassphraseAlert, animated: true) 40 | return 41 | } 42 | sshPrivateKeyURL = privateKeyURL 43 | performSegue(withIdentifier: "importSSHKeySegue", sender: self) 44 | } 45 | } 46 | 47 | extension SSHKeyURLImportTableViewController: KeyImporter { 48 | static let keySource = KeySource.url 49 | static let label = "DownloadFromUrl".localize() 50 | 51 | func isReadyToUse() -> Bool { 52 | guard let url = sshPrivateKeyURL else { 53 | Utils.alert(title: "CannotSave".localize(), message: "SetPrivateKeyUrl.".localize(), controller: self) 54 | return false 55 | } 56 | guard url.scheme == "https" || url.scheme == "http" else { 57 | Utils.alert(title: "CannotSave".localize(), message: "UseEitherHttpsOrHttp.".localize(), controller: self) 58 | return false 59 | } 60 | return true 61 | } 62 | 63 | func importKeys() throws { 64 | Defaults.gitSSHPrivateKeyURL = sshPrivateKeyURL 65 | try KeyFileManager.PrivateSSH.importKey(from: Defaults.gitSSHPrivateKeyURL!) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pass/Controllers/SettingsSplitViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsSplitViewController.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 6/21/17. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SettingsSplitViewController: UISplitViewController, UISplitViewControllerDelegate { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | delegate = self 15 | preferredDisplayMode = .allVisible 16 | } 17 | 18 | func splitViewController( 19 | _: UISplitViewController, 20 | collapseSecondary _: UIViewController, 21 | onto _: UIViewController 22 | ) -> Bool { 23 | // Return true to prevent UIKit from applying its default behavior 24 | true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pass/Controllers/SpecialThanksTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpecialThanksTableViewController.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 9/2/2017. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SpecialThanksTableViewController: BasicStaticTableViewController { 12 | let openSourceComponents = [ 13 | [ 14 | "Contributors".localize(), 15 | "https://github.com/mssun/passforios/graphs/contributors", 16 | ], 17 | [ 18 | "Password Store", 19 | "https://passwordstore.org", 20 | ], 21 | [ 22 | "Icon8", 23 | "https://icons8.com", 24 | ], 25 | [ 26 | "FlatIcon", 27 | "https://www.flaticon.com", 28 | ], 29 | ] 30 | 31 | override func viewDidLoad() { 32 | tableData.append([]) 33 | for item in openSourceComponents { 34 | tableData[0].append( 35 | [CellDataKey.action: "link", CellDataKey.title: item[0], CellDataKey.link: item[1]] 36 | ) 37 | } 38 | super.viewDidLoad() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pass/Helpers/Objective-CBridgingHeader.h: -------------------------------------------------------------------------------- 1 | // 2 | // Objective-CBridgingHeader.h 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 19/1/2017. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | #ifndef Objective_CBridgingHeader_h 10 | #define Objective_CBridgingHeader_h 11 | 12 | @import ObjectiveGit; 13 | 14 | #endif /* Objective_CBridgingHeader_h */ 15 | -------------------------------------------------------------------------------- /pass/Helpers/PasswordAlertPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordAlertPresenter.swift 3 | // pass 4 | // 5 | // Created by Danny Moesch on 23.08.20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import SVProgressHUD 10 | 11 | protocol PasswordAlertPresenter { 12 | func present(message: String, lastPassword: String?) -> String? 13 | } 14 | 15 | extension PasswordAlertPresenter where Self: UIViewController { 16 | func present(message: String, lastPassword: String?) -> String? { 17 | let sem = DispatchSemaphore(value: 0) 18 | var password: String? 19 | 20 | DispatchQueue.main.async { 21 | SVProgressHUD.dismiss() 22 | let alert = UIAlertController(title: "Password".localize(), message: message, preferredStyle: .alert) 23 | alert.addTextField { 24 | $0.text = lastPassword ?? "" 25 | $0.isSecureTextEntry = true 26 | } 27 | alert.addAction( 28 | .ok { _ in 29 | password = alert.textFields?.first?.text 30 | sem.signal() 31 | } 32 | ) 33 | alert.addAction( 34 | .cancel { _ in 35 | password = nil 36 | sem.signal() 37 | } 38 | ) 39 | self.present(alert, animated: true) 40 | } 41 | 42 | _ = sem.wait(timeout: .distantFuture) 43 | return password 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pass/Helpers/SecurePasteboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecurePasteboard.swift 3 | // pass 4 | // 5 | // Created by Yishi Lin on 2017/7/27. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SecurePasteboard { 12 | static let shared = SecurePasteboard() 13 | 14 | private var backgroundTaskID = UIBackgroundTaskIdentifier.invalid 15 | 16 | func copy(textToCopy: String?, expirationTime: Double = 45) { 17 | // copy to the pasteboard 18 | UIPasteboard.general.string = textToCopy ?? "" 19 | 20 | // clean the pasteboard after expirationTime 21 | guard expirationTime > 0 else { 22 | return 23 | } 24 | 25 | // exit the existing background task, if any 26 | if backgroundTaskID != UIBackgroundTaskIdentifier.invalid { 27 | UIApplication.shared.endBackgroundTask(UIBackgroundTaskIdentifier.invalid) 28 | backgroundTaskID = UIBackgroundTaskIdentifier.invalid 29 | } 30 | 31 | backgroundTaskID = UIApplication.shared.beginBackgroundTask { [weak self] in 32 | UIPasteboard.general.string = "" 33 | UIApplication.shared.endBackgroundTask(UIBackgroundTaskIdentifier.invalid) 34 | self?.backgroundTaskID = UIBackgroundTaskIdentifier.invalid 35 | } 36 | 37 | DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + expirationTime) { [weak self] in 38 | UIPasteboard.general.string = "" 39 | UIApplication.shared.endBackgroundTask(UIBackgroundTaskIdentifier.invalid) 40 | self?.backgroundTaskID = UIBackgroundTaskIdentifier.invalid 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pass/Helpers/UtilsExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UtilsExtension.swift 3 | // pass 4 | // 5 | // Created by Yishi Lin on 13/6/17. 6 | // Copyright © 2017年 Bob Sun. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import passKit 11 | import SVProgressHUD 12 | 13 | extension Utils { 14 | static func alert(title: String, message: String, controller: UIViewController, handler: ((UIAlertAction) -> Void)? = nil, completion: (() -> Void)? = nil) { 15 | SVProgressHUD.dismiss() 16 | let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert) 17 | alert.addAction(UIAlertAction(title: "Ok".localize(), style: UIAlertAction.Style.default, handler: handler)) 18 | controller.present(alert, animated: true, completion: completion) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pass/Models/QRKeyScanner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QRKeyScanner.swift 3 | // pass 4 | // 5 | // Created by Danny Moesch on 19.08.20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | struct QRKeyScanner { 10 | enum Result: Equatable { 11 | case lookingForStart 12 | case wrongKeyType(ScannableKeyType) 13 | case completed 14 | case scanned(Int) 15 | 16 | var message: String { 17 | switch self { 18 | case .lookingForStart: 19 | return "LookingForStartingFrame.".localize() 20 | case let .wrongKeyType(keyType): 21 | return "Scan\(keyType.visibility)Key.".localize() 22 | case .completed: 23 | return "Done".localize() 24 | case let .scanned(count): 25 | return "ScannedQrCodes(%d)".localize(count) 26 | } 27 | } 28 | 29 | var unrolled: (accepted: Bool, message: String) { 30 | if self == .completed { 31 | return (true, message) 32 | } 33 | return (false, message) 34 | } 35 | } 36 | 37 | private var segments = [String]() 38 | private var previousResult = Result.lookingForStart 39 | 40 | let keyType: ScannableKeyType 41 | 42 | init(keyType: ScannableKeyType) { 43 | self.keyType = keyType 44 | } 45 | 46 | var scannedKey: String { 47 | segments.joined() 48 | } 49 | 50 | mutating func add(segment: String) -> Result { 51 | // Skip duplicated segments. 52 | guard segment != segments.last else { 53 | return previousResult 54 | } 55 | 56 | // Check whether we have found the first block. 57 | guard !segments.isEmpty || segment.contains(keyType.headerStart) else { 58 | // Check whether we are scanning the wrong key type. 59 | if let counterKeyType = keyType.counterType, segment.contains(counterKeyType.headerStart) { 60 | previousResult = .wrongKeyType(counterKeyType) 61 | } 62 | return previousResult 63 | } 64 | 65 | // Update the list of scanned segments and return. 66 | segments.append(segment) 67 | if scannedKey.contains(keyType.footerStart), scannedKey.trimmed.hasSuffix(keyType.footerEnd) { 68 | return .completed 69 | } 70 | previousResult = .scanned(segments.count) 71 | return previousResult 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pass/Models/ScannableKeyType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScannableKeyType.swift 3 | // pass 4 | // 5 | // Created by Danny Moesch on 19.08.20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | enum ScannableKeyType { 10 | case pgpPublic 11 | case pgpPrivate 12 | case sshPrivate 13 | 14 | var visibility: String { 15 | switch self { 16 | case .pgpPublic: 17 | return "Public" 18 | case .pgpPrivate, .sshPrivate: 19 | return "Private" 20 | } 21 | } 22 | 23 | var headerStart: String { 24 | switch self { 25 | case .pgpPrivate, .pgpPublic: 26 | return "-----BEGIN PGP \(visibility.uppercased()) KEY BLOCK-----" 27 | case .sshPrivate: 28 | return "-----BEGIN" 29 | } 30 | } 31 | 32 | var footerStart: String { 33 | switch self { 34 | case .pgpPrivate, .pgpPublic: 35 | return "-----END PGP \(visibility.uppercased())" 36 | case .sshPrivate: 37 | return "-----END" 38 | } 39 | } 40 | 41 | var footerEnd: String { 42 | switch self { 43 | case .pgpPrivate, .pgpPublic: 44 | return "KEY BLOCK-----" 45 | case .sshPrivate: 46 | return "KEY-----" 47 | } 48 | } 49 | 50 | var counterType: Self? { 51 | switch self { 52 | case .pgpPublic: 53 | return .pgpPrivate 54 | case .pgpPrivate: 55 | return .pgpPublic 56 | case .sshPrivate: 57 | return nil 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pass/Services/PasswordEncryptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordEncryptor.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 17/1/2021. 6 | // Copyright © 2021 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | 11 | func encryptPassword(in controller: UIViewController, with password: Password, keyID: String? = nil, completion: @escaping (() -> Void)) { 12 | DispatchQueue.global(qos: .userInitiated).async { 13 | do { 14 | _ = try PasswordStore.shared.add(password: password, keyID: keyID) 15 | DispatchQueue.main.async { 16 | completion() 17 | } 18 | } catch let AppError.pgpPublicKeyNotFound(keyID: key) { 19 | DispatchQueue.main.async { 20 | let alert = UIAlertController(title: "Cannot Encrypt Password", message: AppError.pgpPublicKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert) 21 | alert.addAction(UIAlertAction.cancelAndPopView(controller: controller)) 22 | let selectKey = UIAlertAction.selectKey(controller: controller) { action in 23 | encryptPassword(in: controller, with: password, keyID: action.title, completion: completion) 24 | } 25 | alert.addAction(selectKey) 26 | 27 | controller.present(alert, animated: true) 28 | } 29 | return 30 | } catch { 31 | DispatchQueue.main.async { 32 | Utils.alert(title: "Error".localize(), message: error.localizedDescription, controller: controller, completion: nil) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pass/Services/PasswordManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordManager.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 17/1/2021. 6 | // Copyright © 2021 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | import SVProgressHUD 11 | import UIKit 12 | 13 | class PasswordManager { 14 | private let viewController: UIViewController 15 | 16 | init(viewController: UIViewController) { 17 | self.viewController = viewController 18 | } 19 | 20 | func providePasswordPasteboard(with passwordPath: String) { 21 | decryptPassword(in: viewController, with: passwordPath) { password in 22 | SecurePasteboard.shared.copy(textToCopy: password.password) 23 | SVProgressHUD.setDefaultMaskType(.black) 24 | SVProgressHUD.setDefaultStyle(.dark) 25 | SVProgressHUD.showSuccess(withStatus: "PasswordCopiedToPasteboard.".localize()) 26 | SVProgressHUD.dismiss(withDelay: 1) 27 | } 28 | } 29 | 30 | func addPassword(with password: Password) { 31 | encryptPassword(in: viewController, with: password) { 32 | SVProgressHUD.setDefaultMaskType(.black) 33 | SVProgressHUD.setDefaultStyle(.light) 34 | SVProgressHUD.showSuccess(withStatus: "Done".localize()) 35 | SVProgressHUD.dismiss(withDelay: 1) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pass/Views/FillPasswordTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FillPasswordTableViewCell.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 11/2/2017. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | import UIKit 11 | 12 | protocol FillPasswordTableViewCellDelegate: AnyObject { 13 | func generateAndCopyPassword() 14 | func showHidePasswordSettings() 15 | } 16 | 17 | class FillPasswordTableViewCell: UITableViewCell { 18 | @IBOutlet var contentTextField: UITextField! 19 | @IBOutlet var settingButton: UIButton! 20 | @IBOutlet var generateButton: UIButton! 21 | 22 | weak var delegate: FillPasswordTableViewCellDelegate? 23 | 24 | override func awakeFromNib() { 25 | super.awakeFromNib() 26 | // Initialization code 27 | contentTextField.font = Globals.passwordFont 28 | 29 | // Force aspect ratio of button images 30 | settingButton.imageView?.contentMode = .scaleAspectFit 31 | generateButton.imageView?.contentMode = .scaleAspectFit 32 | } 33 | 34 | @IBAction 35 | private func generatePassword(_: UIButton) { 36 | delegate?.generateAndCopyPassword() 37 | } 38 | 39 | @IBAction 40 | private func showHidePasswordSettings() { 41 | delegate?.showHidePasswordSettings() 42 | } 43 | 44 | // re-color 45 | @IBAction 46 | private func textFieldDidChange(_ sender: UITextField) { 47 | contentTextField.attributedText = Utils.attributedPassword(plainPassword: sender.text ?? "") 48 | } 49 | 50 | func getContent() -> String? { 51 | contentTextField.attributedText?.string 52 | } 53 | 54 | func setContent(content: String?) { 55 | contentTextField.attributedText = Utils.attributedPassword(plainPassword: content ?? "") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pass/Views/PasswordDetailTitleTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordDetailTitleTableViewCell.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 6/2/2017. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PasswordDetailTitleTableViewCell: UITableViewCell { 12 | @IBOutlet var categoryLabel: UILabel! 13 | @IBOutlet var nameLabel: UILabel! 14 | @IBOutlet var passwordImageImageView: UIImageView! 15 | @IBOutlet var labelImageConstraint: NSLayoutConstraint! 16 | @IBOutlet var labelCellConstraint: NSLayoutConstraint! 17 | } 18 | -------------------------------------------------------------------------------- /pass/Views/PasswordGeneratorUISlider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordGeneratorUISlider.swift 3 | // 4 | 5 | import UIKit 6 | 7 | class PasswordGeneratorUISlider: UISlider { 8 | override func draw(_: CGRect) { 9 | transform = CGAffineTransform(scaleX: 0.8, y: 0.8) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pass/Views/PasswordGeneratorUISwitch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordGeneratorUISwitch.swift 3 | // 4 | 5 | import UIKit 6 | 7 | class PasswordGeneratorUISwitch: UISwitch { 8 | override func draw(_: CGRect) { 9 | transform = CGAffineTransform(scaleX: 0.8, y: 0.8) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pass/Views/PasswordTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordTableViewCell.swift 3 | // pass 4 | // 5 | // Created by Sun, Mingshen on 12/31/20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | 11 | class PasswordTableViewCell: UITableViewCell { 12 | func configure(with entry: PasswordTableEntry) { 13 | textLabel?.font = UIFont.preferredFont(forTextStyle: .body) 14 | textLabel?.text = entry.passwordEntity.isSynced ? entry.title : "↻ \(entry.title)" 15 | textLabel?.adjustsFontForContentSizeCategory = true 16 | 17 | accessoryType = .none 18 | detailTextLabel?.textColor = UIColor.lightGray 19 | detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .footnote) 20 | detailTextLabel?.adjustsFontForContentSizeCategory = true 21 | detailTextLabel?.text = entry.categoryText 22 | 23 | if entry.isDir { 24 | accessoryType = .disclosureIndicator 25 | textLabel?.font = UIFont.systemFont(ofSize: UIFont.preferredFont(forTextStyle: .body).pointSize, weight: .medium) 26 | detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .body) 27 | detailTextLabel?.text = "\(entry.passwordEntity.children.count)" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pass/Views/SliderTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SliderTableViewCell.swift 3 | // pass 4 | // 5 | // Created by Yishi Lin on 8/3/17. 6 | // Copyright © 2017 Yishi Lin. All rights reserved. 7 | // 8 | 9 | import passKit 10 | import UIKit 11 | 12 | class SliderTableViewCell: UITableViewCell { 13 | @IBOutlet var titleLabel: UILabel! 14 | @IBOutlet var valueLabel: UILabel! 15 | @IBOutlet var slider: UISlider! 16 | 17 | private var checker: ((Int) -> Bool)! 18 | private var updater: ((Int) -> Void)! 19 | 20 | private weak var delegate: PasswordSettingSliderTableViewCellDelegate! 21 | 22 | @IBAction 23 | private func handleSliderValueChange(_ sender: UISlider) { 24 | let newRoundedValue = Int(sender.value) 25 | // Proceed only if the rounded value gets updated. 26 | guard checker(newRoundedValue) else { 27 | return 28 | } 29 | sender.value = Float(newRoundedValue) 30 | valueLabel.text = "\(newRoundedValue)" 31 | 32 | updater(newRoundedValue) 33 | delegate.generateAndCopyPassword() 34 | } 35 | 36 | override func layoutMarginsDidChange() { 37 | layoutMargins.left = passKit.Globals.passwordGeneratorLeftLayoutMargin 38 | } 39 | 40 | func set(title: String) -> SliderTableViewCell { 41 | titleLabel.text = title 42 | return self 43 | } 44 | 45 | func configureSlider(with configuration: LengthLimits) -> SliderTableViewCell { 46 | slider.minimumValue = Float(configuration.min) 47 | slider.maximumValue = Float(configuration.max) 48 | return self 49 | } 50 | 51 | func set(initialValue: Int) -> SliderTableViewCell { 52 | slider.value = Float(initialValue) 53 | valueLabel.text = String(initialValue) 54 | return self 55 | } 56 | 57 | func checkNewValue(with checker: @escaping (Int) -> Bool) -> SliderTableViewCell { 58 | self.checker = checker 59 | return self 60 | } 61 | 62 | func updateNewValue(using updater: @escaping (Int) -> Void) -> SliderTableViewCell { 63 | self.updater = updater 64 | return self 65 | } 66 | 67 | func delegate(to delegate: PasswordSettingSliderTableViewCellDelegate) -> SliderTableViewCell { 68 | self.delegate = delegate 69 | return self 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pass/Views/SwitchTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwitchTableViewCell.swift 3 | // pass 4 | // 5 | // Created by Danny Moesch on 28.02.20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | import UIKit 11 | 12 | class SwitchTableViewCell: UITableViewCell { 13 | @IBOutlet var titleLabel: UILabel! 14 | @IBOutlet var controlSwitch: UISwitch! 15 | 16 | private var updater: ((Bool) -> Void)! 17 | 18 | private weak var delegate: PasswordSettingSliderTableViewCellDelegate! 19 | 20 | @IBAction 21 | private func switchValueChanged(_: Any) { 22 | updater(controlSwitch.isOn) 23 | delegate.generateAndCopyPassword() 24 | } 25 | 26 | func set(title: String) -> SwitchTableViewCell { 27 | titleLabel.text = title 28 | return self 29 | } 30 | 31 | override func layoutMarginsDidChange() { 32 | layoutMargins.left = passKit.Globals.passwordGeneratorLeftLayoutMargin 33 | } 34 | 35 | func set(initialValue: Bool) -> SwitchTableViewCell { 36 | controlSwitch.isOn = initialValue 37 | return self 38 | } 39 | 40 | func updateNewValue(using updater: @escaping (Bool) -> Void) -> SwitchTableViewCell { 41 | self.updater = updater 42 | return self 43 | } 44 | 45 | func delegate(to delegate: PasswordSettingSliderTableViewCellDelegate) -> SwitchTableViewCell { 46 | self.delegate = delegate 47 | return self 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pass/Views/TextFieldTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFieldTableViewCell.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 10/2/2017. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TextFieldTableViewCell: UITableViewCell { 12 | @IBOutlet var contentTextField: UITextField! 13 | 14 | func getContent() -> String? { 15 | contentTextField.text 16 | } 17 | 18 | func setContent(content: String?) { 19 | contentTextField.text = content 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pass/Views/TextViewTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextViewTableViewCell.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 11/2/2017. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TextViewTableViewCell: UITableViewCell { 12 | @IBOutlet var contentTextView: UITextView! 13 | 14 | override func awakeFromNib() { 15 | super.awakeFromNib() 16 | contentTextView.textContainer.lineFragmentPadding = 0 17 | contentTextView.textContainerInset = .zero 18 | } 19 | 20 | func getContent() -> String? { 21 | contentTextView.text 22 | } 23 | 24 | func setContent(content: String?) { 25 | contentTextView.text = content 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pass/Views/UICodeHighlightingLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICodeHighlightingLabel.swift 3 | // pass 4 | // 5 | // Created by Danny Moesch on 20.01.19. 6 | // Copyright © 2019 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | import UIKit 11 | 12 | class UICodeHighlightingLabel: UILocalizedLabel { 13 | private static let CODE_ATTRIBUTES: [NSAttributedString.Key: Any] = [.font: UIFont(name: "Menlo-Regular", size: 12)!] 14 | private static let ATTRIBUTED_NEWLINE = NSAttributedString(string: "\n") 15 | 16 | override func awakeFromNib() { 17 | super.awakeFromNib() 18 | guard let text else { 19 | return 20 | } 21 | attributedText = formatCode(in: text) 22 | } 23 | 24 | /// Format code sections in a multiline string block. 25 | /// 26 | /// A line in the string is interpreted as a code section if it starts with two spaces. 27 | /// 28 | /// - Parameter text: Multiline string block 29 | /// - Returns: Same multiline string block with code sections formatted 30 | private func formatCode(in text: String) -> NSMutableAttributedString { 31 | let formattedText = text.splitByNewline() 32 | .map { line -> NSAttributedString in 33 | if line.starts(with: " ") { 34 | return NSAttributedString(string: line, attributes: Self.CODE_ATTRIBUTES) 35 | } 36 | return NSAttributedString(string: line) 37 | } 38 | .reduce(into: NSMutableAttributedString(string: "")) { 39 | $0.append($1) 40 | $0.append(Self.ATTRIBUTED_NEWLINE) 41 | } 42 | formattedText.deleteCharacters(in: NSRange(location: formattedText.length - 1, length: 1)) 43 | return formattedText 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pass/Views/UILocalizedLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UILocalizedLabel.swift 3 | // pass 4 | // 5 | // Created by Danny Moesch on 20.01.19. 6 | // Copyright © 2019 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | import UIKit 11 | 12 | class UILocalizedLabel: UILabel { 13 | override func awakeFromNib() { 14 | super.awakeFromNib() 15 | text = text?.localize() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pass/de.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | pass 4 | 5 | Created by Danny Moesch on 19.01.19. 6 | Copyright © 2019 Bob Sun. All rights reserved. 7 | */ 8 | 9 | "NSCameraUsageDescription" = "Für das Scannen von QR-Codes wird die Erlaubnis zur Nutzung der Kamera benötigt."; 10 | "NSFaceIDUsageDescription" = "Erlaube Face ID, um Pass zu entsperren."; 11 | -------------------------------------------------------------------------------- /pass/de.lproj/Intents.strings: -------------------------------------------------------------------------------- 1 | "0r0NKK" = "Die Übertragung von lokalen Daten zum entfernten Repository schlug fehl."; 2 | 3 | "2jFDLY" = "Synchronisiere das Repository."; 4 | 5 | "H2uxch" = "Es wurde kein Passwort für die Kommunikation mit dem Server gespeichert."; 6 | 7 | "HO0DF8" = "Es wurde kein Passwort für die Kommunikation mit dem Server gespeichert."; 8 | 9 | "IhvhFk" = "Die Synchronisation war erfolgreich."; 10 | 11 | "JthclV" = "Es gibt kein lokales Repository."; 12 | 13 | "KFGEkd" = "Synchronisiere Repository"; 14 | 15 | "O9MV3m" = "Die Synchronisation schlug fehl."; 16 | 17 | "URQN5E" = "Die Synchronisation war erfolgreich."; 18 | 19 | "k4AscG" = "Das Laden von Änderungen vom entfernten Server schlug fehl."; 20 | 21 | "kbRWJu" = "Das Laden von Änderungen vom entfernten Server schlug fehl."; 22 | 23 | "nuN5Fq" = "Synchronisiere das Repository."; 24 | 25 | "qr6SIW" = "Es gibt kein lokales Repository."; 26 | 27 | "wB7FJn" = "Die Synchronisation schlug fehl."; 28 | 29 | "z8bZQR" = "Die Übertragung von lokalen Daten zum entfernten Repository schlug fehl."; 30 | 31 | -------------------------------------------------------------------------------- /pass/de.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ScannedQrCodes(%d) 6 | 7 | NSStringLocalizedFormatKey 8 | %#@codes@ 9 | codes 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | NSStringFormatValueTypeKey 14 | d 15 | zero 16 | Keine gescannten QR-Codes 17 | one 18 | Ein gescannter QR-Code 19 | other 20 | %d gescannte QR-Codes 21 | 22 | 23 | DiscardedCommits(%d) 24 | 25 | NSStringLocalizedFormatKey 26 | %#@commits@ 27 | commits 28 | 29 | NSStringFormatSpecTypeKey 30 | NSStringPluralRuleType 31 | NSStringFormatValueTypeKey 32 | d 33 | zero 34 | Keine lokalen Commits 35 | one 36 | Ein Commit verworfen 37 | other 38 | %d Commits verworfen 39 | 40 | 41 | HiddenFields(%d) 42 | 43 | NSStringLocalizedFormatKey 44 | %#@fields@ 45 | fields 46 | 47 | NSStringFormatSpecTypeKey 48 | NSStringPluralRuleType 49 | NSStringFormatValueTypeKey 50 | d 51 | zero 52 | Keine verborgenen Felder 53 | one 54 | Ein verborgenes Feld 55 | other 56 | %d verborgene Felder 57 | 58 | 59 | WrongAttempts(%d) 60 | 61 | NSStringLocalizedFormatKey 62 | %#@attempts@ 63 | attempts 64 | 65 | NSStringFormatSpecTypeKey 66 | NSStringPluralRuleType 67 | NSStringFormatValueTypeKey 68 | d 69 | zero 70 | Kein falscher Entsperrversuch 71 | one 72 | Ein falscher Entsperrversuch 73 | other 74 | %d falsche Entsperrversuche 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /pass/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | pass 4 | 5 | Created by Danny Moesch on 19.01.19. 6 | Copyright © 2019 Bob Sun. All rights reserved. 7 | */ 8 | 9 | "NSCameraUsageDescription" = "We need to access your camera for scanning QR codes."; 10 | "NSFaceIDUsageDescription" = "Enable access to Face ID to unlock Pass."; 11 | -------------------------------------------------------------------------------- /pass/en.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ScannedQrCodes(%d) 6 | 7 | NSStringLocalizedFormatKey 8 | %#@codes@ 9 | codes 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | NSStringFormatValueTypeKey 14 | d 15 | zero 16 | No scanned QR code 17 | one 18 | One scanned QR code 19 | other 20 | %d scanned QR codes 21 | 22 | 23 | DiscardedCommits(%d) 24 | 25 | NSStringLocalizedFormatKey 26 | %#@commits@ 27 | commits 28 | 29 | NSStringFormatSpecTypeKey 30 | NSStringPluralRuleType 31 | NSStringFormatValueTypeKey 32 | d 33 | zero 34 | No local commits 35 | one 36 | Discarded one commit 37 | other 38 | Discarded %d commits 39 | 40 | 41 | HiddenFields(%d) 42 | 43 | NSStringLocalizedFormatKey 44 | %#@fields@ 45 | fields 46 | 47 | NSStringFormatSpecTypeKey 48 | NSStringPluralRuleType 49 | NSStringFormatValueTypeKey 50 | d 51 | zero 52 | No hidden field 53 | one 54 | One hidden field 55 | other 56 | %d hidden fields 57 | 58 | 59 | WrongAttempts(%d) 60 | 61 | NSStringLocalizedFormatKey 62 | %#@attempts@ 63 | attempts 64 | 65 | NSStringFormatSpecTypeKey 66 | NSStringPluralRuleType 67 | NSStringFormatValueTypeKey 68 | d 69 | zero 70 | No wrong attempt 71 | one 72 | One wrong attempt 73 | other 74 | %d wrong attempts 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /pass/en.lproj/Main.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/pass/en.lproj/Main.strings -------------------------------------------------------------------------------- /pass/it.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | pass 4 | 5 | Created by Danny Moesch on 19.01.19. 6 | Copyright © 2019 Bob Sun. All rights reserved. 7 | */ 8 | 9 | "NSCameraUsageDescription" = "L'accesso alla fotocamera è richiesto per scansionare codici QR."; 10 | "NSFaceIDUsageDescription" = "Attiva l'accesso al Face ID per sbloccare Pass."; 11 | -------------------------------------------------------------------------------- /pass/it.lproj/Intents.strings: -------------------------------------------------------------------------------- 1 | "0r0NKK" = "Impossibile eseguire il push dei cambiamenti al repository remoto."; 2 | 3 | "2jFDLY" = "Sincronizzazione del repository con il server."; 4 | 5 | "H2uxch" = "Nessuna password è salvata per comunicare al repository remoto."; 6 | 7 | "HO0DF8" = "Nessuna password è salvata per comunicare al repository remoto."; 8 | 9 | "IhvhFk" = "Sincronizzazione completata."; 10 | 11 | "JthclV" = "Nessun repository locale."; 12 | 13 | "KFGEkd" = "Sincronizza repository"; 14 | 15 | "O9MV3m" = "La sincronizzazione è fallita."; 16 | 17 | "URQN5E" = "Sincronizzazione completata."; 18 | 19 | "k4AscG" = "Impossibile eseguire il pull dei cambiamenti dal repository remoto."; 20 | 21 | "kbRWJu" = "Impossibile eseguire il pull dei cambiamenti dal repository remoto."; 22 | 23 | "nuN5Fq" = "Sincronizza il repository."; 24 | 25 | "qr6SIW" = "Nessun repository locale."; 26 | 27 | "wB7FJn" = "La sincronizzazione è fallita."; 28 | 29 | "z8bZQR" = "Impossibile eseguire il push dei cambiamenti al repository remoto."; 30 | 31 | -------------------------------------------------------------------------------- /pass/it.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ScannedQrCodes(%d) 6 | 7 | NSStringLocalizedFormatKey 8 | %#@codes@ 9 | codes 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | NSStringFormatValueTypeKey 14 | d 15 | zero 16 | Nessun codice QR scansionato 17 | one 18 | Un codice QR scansionato 19 | other 20 | %d codici QR scansionati 21 | 22 | 23 | DiscardedCommits(%d) 24 | 25 | NSStringLocalizedFormatKey 26 | %#@commits@ 27 | commits 28 | 29 | NSStringFormatSpecTypeKey 30 | NSStringPluralRuleType 31 | NSStringFormatValueTypeKey 32 | d 33 | zero 34 | Nessun commit locale 35 | one 36 | Un commit scartato 37 | other 38 | %d commit scartati 39 | 40 | 41 | HiddenFields(%d) 42 | 43 | NSStringLocalizedFormatKey 44 | %#@fields@ 45 | fields 46 | 47 | NSStringFormatSpecTypeKey 48 | NSStringPluralRuleType 49 | NSStringFormatValueTypeKey 50 | d 51 | zero 52 | Nessun campo nascosto 53 | one 54 | Un campo nascosto 55 | other 56 | %d campi nascosti 57 | 58 | 59 | WrongAttempts(%d) 60 | 61 | NSStringLocalizedFormatKey 62 | %#@attempts@ 63 | attempts 64 | 65 | NSStringFormatSpecTypeKey 66 | NSStringPluralRuleType 67 | NSStringFormatValueTypeKey 68 | d 69 | zero 70 | Nessun tentativo errato 71 | one 72 | Un tentativo errato 73 | other 74 | %d tentativi errati 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /pass/pass.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.authentication-services.autofill-credential-provider 6 | 7 | com.apple.developer.nfc.readersession.formats 8 | 9 | TAG 10 | 11 | com.apple.developer.siri 12 | 13 | com.apple.security.application-groups 14 | 15 | group.me.mssun.passforios 16 | 17 | keychain-access-groups 18 | 19 | $(AppIdentifierPrefix)group.me.mssun.passforios 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /pass/passBeta.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.authentication-services.autofill-credential-provider 6 | 7 | com.apple.developer.nfc.readersession.formats 8 | 9 | TAG 10 | 11 | com.apple.developer.siri 12 | 13 | com.apple.security.application-groups 14 | 15 | group.me.mssun.passforiosbeta 16 | 17 | keychain-access-groups 18 | 19 | $(AppIdentifierPrefix)group.me.mssun.passforiosbeta 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /passAutoFillExtension/Controllers/PasscodeExtensionDisplay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeExtensionDisplay.swift 3 | // passAutoFillExtension 4 | // 5 | // Created by Yishi Lin on 14/6/17. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | 11 | class PasscodeExtensionDisplay { 12 | private let passcodeLockVC: PasscodeLockViewControllerForExtension 13 | 14 | init(extensionContext: NSExtensionContext) { 15 | self.passcodeLockVC = PasscodeLockViewControllerForExtension(extensionContext: extensionContext) 16 | passcodeLockVC.setCancellable(true) 17 | } 18 | 19 | // present the passcode lock view if passcode is set and the view controller is not presented 20 | func presentPasscodeLockIfNeeded(_ sender: UIViewController, before: (() -> Void)? = nil, after: (() -> Void)? = nil) { 21 | if PasscodeLock.shared.hasPasscode { 22 | before?() 23 | passcodeLockVC.successCallback = after 24 | passcodeLockVC.modalPresentationStyle = .fullScreen 25 | sender.parent?.present(passcodeLockVC, animated: false) 26 | } else { 27 | after?() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /passAutoFillExtension/Controllers/PasscodeLockViewControllerForExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeLockViewControllerForExtension.swift 3 | // passAutoFillExtension 4 | // 5 | // Created by Danny Moesch on 24.08.21. 6 | // Copyright © 2021 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | 11 | class PasscodeLockViewControllerForExtension: PasscodeLockViewController { 12 | var originalExtensionContext: NSExtensionContext! 13 | 14 | convenience init(extensionContext: NSExtensionContext) { 15 | self.init() 16 | self.originalExtensionContext = extensionContext 17 | } 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | cancelButton?.removeTarget(nil, action: nil, for: .allEvents) 22 | // cancel means cancel the extension 23 | cancelButton?.addTarget(self, action: #selector(cancelExtension), for: .touchUpInside) 24 | } 25 | 26 | @objc 27 | func cancelExtension() { 28 | originalExtensionContext.cancelRequest(withError: NSError(domain: "PassExtension", code: 0)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /passAutoFillExtension/Controllers/PasswordsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordsViewController.swift 3 | // passAutoFillExtension 4 | // 5 | // Created by Sun, Mingshen on 12/31/20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import AuthenticationServices 10 | import passKit 11 | import UIKit 12 | 13 | class PasswordsViewController: UIViewController { 14 | @IBOutlet var tableView: UITableView! 15 | 16 | var dataSource: PasswordsTableDataSource! 17 | weak var selectionDelegate: PasswordSelectionDelegate? 18 | 19 | lazy var searchController: UISearchController = { 20 | let uiSearchController = UISearchController(searchResultsController: nil) 21 | uiSearchController.searchBar.isTranslucent = true 22 | uiSearchController.obscuresBackgroundDuringPresentation = false 23 | uiSearchController.searchBar.sizeToFit() 24 | uiSearchController.searchBar.searchTextField.clearButtonMode = .whileEditing 25 | return uiSearchController 26 | }() 27 | 28 | lazy var searchBar: UISearchBar = self.searchController.searchBar 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | 33 | navigationItem.searchController = searchController 34 | navigationItem.hidesSearchBarWhenScrolling = false 35 | 36 | searchBar.delegate = self 37 | 38 | tableView.delegate = self 39 | tableView.dataSource = dataSource 40 | } 41 | 42 | func showPasswordsWithSuggestion(matching text: String) { 43 | dataSource.showTableEntriesWithSuggestion(matching: text) 44 | tableView.reloadData() 45 | } 46 | } 47 | 48 | extension PasswordsViewController: UISearchBarDelegate { 49 | func searchBar(_: UISearchBar, textDidChange searchText: String) { 50 | dataSource.showTableEntries(matching: searchText) 51 | tableView.reloadData() 52 | } 53 | 54 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { 55 | searchBar.resignFirstResponder() 56 | } 57 | 58 | func searchBarCancelButtonClicked(_: UISearchBar) { 59 | dataSource.showTableEntries(matching: "") 60 | tableView.reloadData() 61 | } 62 | } 63 | 64 | extension PasswordsViewController: UITableViewDelegate { 65 | func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { 66 | tableView.deselectRow(at: indexPath, animated: true) 67 | let entry = dataSource.tableEntry(at: indexPath) 68 | UIImpactFeedbackGenerator(style: .medium).impactOccurred() 69 | selectionDelegate?.selected(password: entry) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /passAutoFillExtension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Pass 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | 0 23 | NSExtension 24 | 25 | NSExtensionMainStoryboard 26 | MainInterface 27 | NSExtensionPointIdentifier 28 | com.apple.authentication-services-credential-provider-ui 29 | 30 | NSFaceIDUsageDescription 31 | Enable access to Face ID to unlock Pass. 32 | UIApplicationSceneManifest 33 | 34 | UIApplicationSupportsMultipleScenes 35 | 36 | UISceneConfigurations 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /passAutoFillExtension/Protocols/PasswordSelectionDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordSelectionDelegate.swift 3 | // passAutoFillExtension 4 | // 5 | // Created by Sun, Mingshen on 12/31/20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import passKit 10 | 11 | protocol PasswordSelectionDelegate: AnyObject { 12 | func selected(password: PasswordTableEntry) 13 | } 14 | -------------------------------------------------------------------------------- /passAutoFillExtension/Services/CredentialProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CredentialProvider.swift 3 | // passAutoFillExtension 4 | // 5 | // Created by Sun, Mingshen on 1/2/21. 6 | // Copyright © 2021 Bob Sun. All rights reserved. 7 | // 8 | 9 | import AuthenticationServices 10 | import passKit 11 | 12 | class CredentialProvider { 13 | private let viewController: UIViewController 14 | private let extensionContext: ASCredentialProviderExtensionContext 15 | private let afterDecryption: (Password) -> Void 16 | 17 | var identifier: ASCredentialServiceIdentifier? 18 | 19 | init(viewController: UIViewController, extensionContext: ASCredentialProviderExtensionContext, afterDecryption: @escaping (Password) -> Void) { 20 | self.viewController = viewController 21 | self.extensionContext = extensionContext 22 | self.afterDecryption = afterDecryption 23 | } 24 | 25 | func credentials(for identity: ASPasswordCredentialIdentity) { 26 | guard let recordIdentifier = identity.recordIdentifier else { 27 | return 28 | } 29 | 30 | decryptPassword(in: viewController, with: recordIdentifier) { password in 31 | self.extensionContext.completeRequest(withSelectedCredential: .from(password)) 32 | self.afterDecryption(password) 33 | } 34 | } 35 | 36 | func persistAndProvideCredentials(with passwordPath: String) { 37 | decryptPassword(in: viewController, with: passwordPath) { password in 38 | if let identifier = self.identifier { 39 | ASCredentialIdentityStore.shared.getState { state in 40 | guard state.isEnabled else { 41 | return 42 | } 43 | let credentialIdentity = ASPasswordCredentialIdentity( 44 | serviceIdentifier: identifier, 45 | user: password.getUsernameForCompletion(), 46 | recordIdentifier: passwordPath 47 | ) 48 | ASCredentialIdentityStore.shared.saveCredentialIdentities([credentialIdentity]) 49 | } 50 | } 51 | self.extensionContext.completeRequest(withSelectedCredential: .from(password)) 52 | self.afterDecryption(password) 53 | } 54 | } 55 | } 56 | 57 | extension ASPasswordCredential { 58 | static func from(_ password: Password) -> ASPasswordCredential { 59 | ASPasswordCredential(user: password.getUsernameForCompletion(), password: password.password) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /passAutoFillExtension/passAutoFillExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.authentication-services.autofill-credential-provider 6 | 7 | com.apple.security.application-groups 8 | 9 | group.me.mssun.passforios 10 | 11 | keychain-access-groups 12 | 13 | $(AppIdentifierPrefix)group.me.mssun.passforios 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /passAutoFillExtension/passBetaAutoFillExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.authentication-services.autofill-credential-provider 6 | 7 | com.apple.security.application-groups 8 | 9 | group.me.mssun.passforiosbeta 10 | 11 | keychain-access-groups 12 | 13 | $(AppIdentifierPrefix)group.me.mssun.passforiosbeta 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-1024@1x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passExtension/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png -------------------------------------------------------------------------------- /passExtension/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /passExtension/Helpers/ExtensionConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionConstants.swift 3 | // passExtension 4 | // 5 | // Created by Yishi Lin on 2017/6/23. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | // This file contains constants from https://github.com/agilebits/onepassword-app-extension/ 9 | 10 | enum PassExtensionActions { 11 | static let findLogin = "org.appextension.find-login-action" 12 | static let saveLogin = "org.appextension.save-login-action" 13 | static let changePassword = "org.appextension.change-password-action" 14 | static let fillWebView = "org.appextension.fill-webview-action" 15 | static let fillBrowser = "org.appextension.fill-browser-action" 16 | } 17 | 18 | enum PassExtensionKey { 19 | // Login Dictionary keys 20 | static let URLStringKey = "url_string" 21 | static let usernameKey = "username" 22 | static let passwordKey = "password" 23 | static let totpKey = "totp" 24 | } 25 | -------------------------------------------------------------------------------- /passExtension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | $(APP_DISPLAY_NAME) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | 0 23 | NSExtension 24 | 25 | NSExtensionAttributes 26 | 27 | NSExtensionActivationRule 28 | SUBQUERY ( 29 | extensionItems, 30 | $extensionItem, 31 | SUBQUERY ( 32 | $extensionItem.attachments, 33 | $attachment, 34 | ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.find-login-action" || 35 | ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" || 36 | ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text" 37 | ).@count == $extensionItem.attachments.@count 38 | ).@count == 1 39 | NSExtensionJavaScriptPreprocessingFile 40 | passProcessor 41 | 42 | NSExtensionMainStoryboard 43 | MainInterface 44 | NSExtensionPointIdentifier 45 | com.apple.ui-services 46 | 47 | NSFaceIDUsageDescription 48 | Enable access to Face ID to unlock Pass. 49 | UIApplicationSceneManifest 50 | 51 | UIApplicationSupportsMultipleScenes 52 | 53 | UISceneConfigurations 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /passExtension/Services/CredentialProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CredentialProvider.swift 3 | // passExtension 4 | // 5 | // Created by Sun, Mingshen on 1/9/21. 6 | // Copyright © 2021 Bob Sun. All rights reserved. 7 | // 8 | 9 | import MobileCoreServices 10 | import passKit 11 | import UIKit 12 | 13 | class CredentialProvider { 14 | private let viewController: UIViewController 15 | private let extensionContext: NSExtensionContext 16 | private let afterDecryption: (Password) -> Void 17 | 18 | init(viewController: UIViewController, extensionContext: NSExtensionContext, afterDecryption: @escaping (Password) -> Void) { 19 | self.viewController = viewController 20 | self.extensionContext = extensionContext 21 | self.afterDecryption = afterDecryption 22 | } 23 | 24 | func provideCredentialsFindLogin(with passwordPath: String) { 25 | decryptPassword(in: viewController, with: passwordPath) { password in 26 | let extensionItem = NSExtensionItem() 27 | var returnDictionary = [ 28 | PassExtensionKey.usernameKey: password.getUsernameForCompletion(), 29 | PassExtensionKey.passwordKey: password.password, 30 | ] 31 | if let totpPassword = password.currentOtp { 32 | returnDictionary[PassExtensionKey.totpKey] = totpPassword 33 | } 34 | extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))] 35 | self.extensionContext.completeRequest(returningItems: [extensionItem]) 36 | self.afterDecryption(password) 37 | } 38 | } 39 | 40 | func provideCredentialsBrowser(with passwordPath: String) { 41 | decryptPassword(in: viewController, with: passwordPath) { password in 42 | Utils.copyToPasteboard(textToCopy: password.password) 43 | // return a dictionary for JavaScript for best-effor fill in 44 | let extensionItem = NSExtensionItem() 45 | let returnDictionary = [ 46 | NSExtensionJavaScriptFinalizeArgumentKey: [ 47 | PassExtensionKey.usernameKey: password.getUsernameForCompletion(), 48 | PassExtensionKey.passwordKey: password.password, 49 | ], 50 | ] 51 | extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))] 52 | self.extensionContext.completeRequest(returningItems: [extensionItem]) 53 | self.afterDecryption(password) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /passExtension/passBetaExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.me.mssun.passforiosbeta 8 | 9 | keychain-access-groups 10 | 11 | $(AppIdentifierPrefix)group.me.mssun.passforiosbeta 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /passExtension/passExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.me.mssun.passforios 8 | 9 | keychain-access-groups 10 | 11 | $(AppIdentifierPrefix)group.me.mssun.passforios 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /passExtension/passProcessor.js: -------------------------------------------------------------------------------- 1 | var PassProcessor = function() {}; 2 | 3 | /** 4 | * Dispatches a synthetic event of a given type on a given element. 5 | * @param {string} type the event type to dispatch 6 | * @param {Element} el the element upon which to dispatch it 7 | */ 8 | var dispatchEvent = function(type, el) { 9 | var evt = document.createEvent('Event'); 10 | evt.initEvent(type, true, true); 11 | el.dispatchEvent(evt); 12 | }; 13 | 14 | PassProcessor.prototype = { 15 | run: function(arguments) { 16 | var url 17 | var error 18 | try { 19 | url = document.URL 20 | } catch (e) { 21 | error = e 22 | } finally { 23 | arguments.completionFunction({"url_string": url, "error": error}); 24 | } 25 | }, 26 | 27 | finalize: function(arguments) { 28 | if (arguments["password"]) { 29 | var passwordElement = document.querySelector("input[type=password]") 30 | if (passwordElement) { 31 | passwordElement.setAttribute('value', arguments["password"]) 32 | passwordElement.value = arguments["password"] 33 | dispatchEvent("input", passwordElement) 34 | dispatchEvent("change", passwordElement) 35 | } 36 | } 37 | 38 | if (arguments["username"]) { 39 | var usernameElement = document.querySelector("input[type=email], input[type=text]") 40 | if (usernameElement) { 41 | usernameElement.setAttribute('value', arguments["username"]) 42 | usernameElement.value = arguments["username"] 43 | dispatchEvent("input", usernameElement) 44 | dispatchEvent("change", usernameElement) 45 | } 46 | } 47 | } 48 | }; 49 | 50 | // The JavaScript file must contain a global object named "ExtensionPreprocessingJS". 51 | var ExtensionPreprocessingJS = new PassProcessor; 52 | -------------------------------------------------------------------------------- /passKit/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /passKit/Assets.xcassets/PasscodeLockViewIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Icon-57.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Icon-60@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Icon-60@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /passKit/Assets.xcassets/PasscodeLockViewIcon.imageset/Icon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passKit/Assets.xcassets/PasscodeLockViewIcon.imageset/Icon-57.png -------------------------------------------------------------------------------- /passKit/Assets.xcassets/PasscodeLockViewIcon.imageset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passKit/Assets.xcassets/PasscodeLockViewIcon.imageset/Icon-60@2x.png -------------------------------------------------------------------------------- /passKit/Assets.xcassets/PasscodeLockViewIcon.imageset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mssun/passforios/4288c4e2188f3e8b5cb1bd27c9d504551215e8e5/passKit/Assets.xcassets/PasscodeLockViewIcon.imageset/Icon-60@3x.png -------------------------------------------------------------------------------- /passKit/Assets.xcassets/Wordlist/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /passKit/Assets.xcassets/Wordlist/eff_long_wordlist.dataset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "data" : [ 7 | { 8 | "idiom" : "universal", 9 | "filename" : "eff_long_wordlist.txt" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /passKit/Assets.xcassets/Wordlist/eff_short_wordlist.dataset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "data" : [ 7 | { 8 | "idiom" : "universal", 9 | "filename" : "eff_short_wordlist.txt", 10 | "universal-type-identifier" : "public.plain-text" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /passKit/Controllers/PasscodeLockPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeLockPresenter.swift 3 | // passKit 4 | // 5 | // Created by Yishi Lin on 10/04/2018. 6 | // Copyright © 2018 Yishi Lin. All rights reserved. 7 | // 8 | // Inspired by SwiftPasscodeLock created by Yanko Dimitrov. 9 | // 10 | 11 | import UIKit 12 | 13 | open class PasscodeLockPresenter { 14 | private var mainWindow: UIWindow? 15 | private var passcodeLockWindow: UIWindow? 16 | 17 | public init(mainWindow window: UIWindow?) { 18 | self.mainWindow = window 19 | } 20 | 21 | open func present(windowLevel: CGFloat?) { 22 | guard PasscodeLock.shared.hasPasscode else { 23 | return 24 | } 25 | 26 | // dismiss the original window 27 | dismiss() 28 | 29 | // new window 30 | mainWindow?.endEditing(true) 31 | passcodeLockWindow = UIWindow(frame: mainWindow!.frame) 32 | moveWindowsToFront(windowLevel: windowLevel) 33 | passcodeLockWindow?.isHidden = false 34 | 35 | // new vc 36 | let passcodeLockVC = PasscodeLockViewController() 37 | let userDismissCompletionCallback = passcodeLockVC.dismissCompletionCallback 38 | passcodeLockVC.dismissCompletionCallback = { [weak self] in 39 | userDismissCompletionCallback?() 40 | self?.dismiss() 41 | } 42 | passcodeLockWindow?.rootViewController = passcodeLockVC 43 | } 44 | 45 | open func dismiss() { 46 | passcodeLockWindow?.isHidden = true 47 | passcodeLockWindow?.rootViewController = nil 48 | } 49 | 50 | private func moveWindowsToFront(windowLevel: CGFloat?) { 51 | let windowLevel = windowLevel ?? UIWindow.Level.normal.rawValue 52 | let maxWinLevel = max(windowLevel, UIWindow.Level.normal.rawValue) 53 | passcodeLockWindow?.windowLevel = UIWindow.Level(rawValue: maxWinLevel + 1) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /passKit/Crypto/ObjectivePGPInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjectivePGPInterface.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 08.09.19. 6 | // Copyright © 2019 Bob Sun. All rights reserved. 7 | // 8 | 9 | import ObjectivePGP 10 | 11 | struct ObjectivePGPInterface: PGPInterface { 12 | private let keyring = ObjectivePGP.defaultKeyring 13 | 14 | init(publicArmoredKey: String, privateArmoredKey: String) throws { 15 | guard let publicKeyData = publicArmoredKey.data(using: .ascii), let privateKeyData = privateArmoredKey.data(using: .ascii) else { 16 | throw AppError.keyImport 17 | } 18 | let publicKeys = try ObjectivePGP.readKeys(from: publicKeyData) 19 | let privateKeys = try ObjectivePGP.readKeys(from: privateKeyData) 20 | keyring.import(keys: publicKeys) 21 | keyring.import(keys: privateKeys) 22 | guard publicKeys.first != nil, privateKeys.first != nil else { 23 | throw AppError.keyImport 24 | } 25 | } 26 | 27 | func decrypt(encryptedData: Data, keyID _: String?, passphrase: String) throws -> Data? { 28 | try ObjectivePGP.decrypt(encryptedData, andVerifySignature: false, using: keyring.keys) { _ in passphrase } 29 | } 30 | 31 | func encrypt(plainData: Data, keyID _: String?) throws -> Data { 32 | let encryptedData = try ObjectivePGP.encrypt(plainData, addSignature: false, using: keyring.keys, passphraseForKey: nil) 33 | if Defaults.encryptInArmored { 34 | return Armor.armored(encryptedData, as: .message).data(using: .ascii)! 35 | } 36 | return encryptedData 37 | } 38 | 39 | func containsPublicKey(with keyID: String) -> Bool { 40 | keyring.findKey(keyID)?.isPublic ?? false 41 | } 42 | 43 | func containsPrivateKey(with keyID: String) -> Bool { 44 | keyring.findKey(keyID)?.isSecret ?? false 45 | } 46 | 47 | var keyID: [String] { 48 | keyring.keys.map(\.keyID.longIdentifier) 49 | } 50 | 51 | var shortKeyID: [String] { 52 | keyring.keys.map(\.keyID.shortIdentifier) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /passKit/Crypto/PGPInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PGPInterface.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 08.09.19. 6 | // Copyright © 2019 Bob Sun. All rights reserved. 7 | // 8 | 9 | protocol PGPInterface { 10 | func decrypt(encryptedData: Data, keyID: String?, passphrase: String) throws -> Data? 11 | 12 | func encrypt(plainData: Data, keyID: String?) throws -> Data 13 | 14 | func containsPublicKey(with keyID: String) -> Bool 15 | 16 | func containsPrivateKey(with keyID: String) -> Bool 17 | 18 | var keyID: [String] { get } 19 | 20 | var shortKeyID: [String] { get } 21 | } 22 | -------------------------------------------------------------------------------- /passKit/Extensions/Array+Slices.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Slices.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 28.02.20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | extension Array { 10 | func slices(count: UInt) -> [ArraySlice] { 11 | guard count != 0 else { 12 | return [] 13 | } 14 | let sizeEach = Int(self.count / Int(count)) 15 | var currentIndex = startIndex 16 | var slices = [ArraySlice]() 17 | for _ in 0 ..< count { 18 | let toIndex = index(currentIndex, offsetBy: sizeEach, limitedBy: endIndex) ?? endIndex 19 | slices.append(self[currentIndex ..< toIndex]) 20 | currentIndex = toIndex 21 | } 22 | if currentIndex != endIndex { 23 | slices[slices.endIndex - 1].append(contentsOf: self[currentIndex ..< endIndex]) 24 | } 25 | return slices 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /passKit/Extensions/Data+Mutable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Mutable.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 08.09.19. 6 | // Copyright © 2019 Bob Sun. All rights reserved. 7 | // 8 | 9 | extension Data { 10 | /// This computed value is only needed because of [this](https://github.com/golang/go/issues/33745) issue in the 11 | /// golang/go repository. It is a workaround until the problem is solved upstream. 12 | /// 13 | /// The data object is converted into an array of bytes and than returned wrapped in an `NSMutableData` object. In 14 | /// thas way Gomobile takes it as it is without copying. The Swift side remains responsible for garbage collection. 15 | var mutable: NSMutableData { 16 | var array = [UInt8](self) 17 | return NSMutableData(bytes: &array, length: count) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /passKit/Extensions/String+Localization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Localization.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 12.01.19. 6 | // Copyright © 2019 Bob Sun. All rights reserved. 7 | // 8 | 9 | public extension String { 10 | func localize() -> String { 11 | // swiftlint:disable:next nslocalizedstring_key 12 | NSLocalizedString(self, bundle: Bundle.main, value: "#\(self)#", comment: "") 13 | } 14 | 15 | func localize(_ firstValue: CVarArg) -> String { 16 | String(format: localize(), firstValue) 17 | } 18 | 19 | func localize(_ firstValue: CVarArg, _ secondValue: CVarArg) -> String { 20 | String(format: localize(), firstValue, secondValue) 21 | } 22 | 23 | func localize(_ error: Error) -> String { 24 | localize(error.localizedDescription) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /passKit/Extensions/String+Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Utilities.swift 3 | // passKit 4 | // 5 | // Created by Yishi Lin on 2018/9/23. 6 | // Copyright © 2018 Bob Sun. All rights reserved. 7 | // 8 | 9 | public extension String { 10 | var trimmed: String { 11 | trimmingCharacters(in: .whitespacesAndNewlines) 12 | } 13 | 14 | func stringByAddingPercentEncodingForRFC3986() -> String? { 15 | let unreserved = "-._~/" 16 | var allowed = CharacterSet.alphanumerics 17 | allowed.insert(charactersIn: unreserved) 18 | return addingPercentEncoding(withAllowedCharacters: allowed) 19 | } 20 | 21 | func splitByNewline() -> [String] { 22 | split(omittingEmptySubsequences: false) { $0 == "\n" || $0 == "\r\n" }.map(Self.init) 23 | } 24 | } 25 | 26 | public extension String { 27 | static func | (left: String, right: String) -> String { 28 | right.isEmpty ? left : left + "\n" + right 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /passKit/Extensions/UIAlertActionExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIAlertActionExtension.swift 3 | // passKit 4 | // 5 | // Created by Sun, Mingshen on 4/17/20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public extension UIAlertAction { 13 | static func cancelAndPopView(controller: UIViewController) -> UIAlertAction { 14 | cancel { _ in 15 | controller.navigationController?.popViewController(animated: true) 16 | } 17 | } 18 | 19 | static func cancel(title: String = "Cancel".localize(), handler: ((UIAlertAction) -> Void)? = nil) -> UIAlertAction { 20 | UIAlertAction(title: title, style: .cancel, handler: handler) 21 | } 22 | 23 | static func dismiss(handler: ((UIAlertAction) -> Void)? = nil) -> UIAlertAction { 24 | cancel(title: "Dismiss".localize(), handler: handler) 25 | } 26 | 27 | static func ok(handler: ((UIAlertAction) -> Void)? = nil) -> UIAlertAction { 28 | UIAlertAction(title: "Ok".localize(), style: .default, handler: handler) 29 | } 30 | 31 | static func remove(handler: ((UIAlertAction) -> Void)? = nil) -> UIAlertAction { 32 | UIAlertAction(title: "Remove".localize(), style: .destructive, handler: handler) 33 | } 34 | 35 | static func okAndPopView(controller: UIViewController) -> UIAlertAction { 36 | ok { _ in 37 | controller.navigationController?.popViewController(animated: true) 38 | } 39 | } 40 | 41 | static func selectKey(controller: UIViewController, handler: ((UIAlertAction) -> Void)?) -> UIAlertAction { 42 | UIAlertAction(title: "Select Key", style: .default) { _ in 43 | let selectKeyAlert = UIAlertController(title: "Select from imported keys", message: nil, preferredStyle: .actionSheet) 44 | try? PGPAgent.shared.getShortKeyID().forEach { keyID in 45 | let action = UIAlertAction(title: keyID, style: .default, handler: handler) 46 | selectKeyAlert.addAction(action) 47 | } 48 | selectKeyAlert.addAction(Self.cancelAndPopView(controller: controller)) 49 | controller.present(selectKeyAlert, animated: true, completion: nil) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /passKit/Extensions/UIAlertControllerExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIAlertControllerExtension.swift 3 | // passKit 4 | // 5 | // Copyright © 2021 Bob Sun. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension UIAlertController { 11 | class func removeConfirmationAlert(title: String, message: String, handler: ((UIAlertAction) -> Void)? = nil) -> UIAlertController { 12 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 13 | alert.addAction(UIAlertAction.remove(handler: handler)) 14 | alert.addAction(UIAlertAction.cancel()) 15 | return alert 16 | } 17 | 18 | class func showErrorAlert(title: String, message: String, completion: ((UIAlertAction) -> Void)? = nil) -> UIAlertController { 19 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 20 | alert.addAction(UIAlertAction.ok(handler: completion)) 21 | return alert 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /passKit/Extensions/UITextFieldExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITextFieldExtension.swift 3 | // passKit 4 | // 5 | // Created by Yishi Lin on 5/4/17. 6 | // Copyright © 2017 Yishi Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | private var kAssociationKeyNextField: UInt8 = 0 13 | 14 | extension UITextField { 15 | @IBOutlet var nextField: UITextField? { 16 | get { 17 | objc_getAssociatedObject(self, &kAssociationKeyNextField) as? UITextField 18 | } 19 | set(newField) { 20 | objc_setAssociatedObject(self, &kAssociationKeyNextField, newField, .OBJC_ASSOCIATION_RETAIN) 21 | } 22 | } 23 | 24 | func shake() { 25 | let animation = CAKeyframeAnimation(keyPath: "transform.translation.x") 26 | animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) 27 | animation.repeatCount = 3 28 | animation.duration = 0.2 / TimeInterval(animation.repeatCount) 29 | animation.autoreverses = true 30 | animation.values = [3, -3] 31 | layer.add(animation, forKey: "shake") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /passKit/Extensions/UIViewControllerExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewControllerExtension.swift 3 | // passKit 4 | // 5 | // Created by Yishi Lin on 5/4/17. 6 | // Copyright © 2017 Yishi Lin. All rights reserved. 7 | // 8 | 9 | public extension UIViewController { 10 | @objc 11 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 12 | if textField.nextField != nil { 13 | textField.nextField?.becomeFirstResponder() 14 | } else { 15 | textField.resignFirstResponder() 16 | } 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /passKit/Extensions/UIViewExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewExtension.swift 3 | // passKit 4 | // 5 | // Created by Yishi Lin on 2018/4/11. 6 | // Copyright © 2018 Yishi Lin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension UIView { 12 | // Save anchors: https://stackoverflow.com/questions/46317061/use-safe-area-layout-programmatically 13 | var safeTopAnchor: NSLayoutYAxisAnchor { 14 | safeAreaLayoutGuide.topAnchor 15 | } 16 | 17 | var safeLeftAnchor: NSLayoutXAxisAnchor { 18 | safeAreaLayoutGuide.leftAnchor 19 | } 20 | 21 | var safeRightAnchor: NSLayoutXAxisAnchor { 22 | safeAreaLayoutGuide.rightAnchor 23 | } 24 | 25 | var safeBottomAnchor: NSLayoutYAxisAnchor { 26 | safeAreaLayoutGuide.bottomAnchor 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /passKit/Helpers/AppError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppError.swift 3 | // passKit 4 | // 5 | // Created by Mingshen Sun on 30/4/2017. 6 | // Copyright © 2017 Bob Sun. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum AppError: Error, Equatable { 12 | case repositoryNotSet 13 | case repositoryRemoteBranchNotFound(branchName: String) 14 | case repositoryBranchNotFound(branchName: String) 15 | case keyImport 16 | case readingFile(fileName: String) 17 | case passwordDuplicated 18 | case gitReset 19 | case gitCommit 20 | case gitCreateSignature 21 | case gitPushNotSuccessful 22 | case pgpPublicKeyNotFound(keyID: String) 23 | case pgpPrivateKeyNotFound(keyID: String) 24 | case yubiKey(YubiKeyError) 25 | case passwordFileNotFound(path: String) 26 | case keyExpiredOrIncompatible 27 | case wrongPassphrase 28 | case wrongPasswordFilename 29 | case decryption 30 | case encryption 31 | case encoding 32 | case other(message: String) 33 | } 34 | 35 | public enum YubiKeyError: Error, Equatable { 36 | case connection(message: String) 37 | case selectApplication(message: String) 38 | case verify(message: String) 39 | case decipher(message: String) 40 | case other(message: String) 41 | } 42 | 43 | extension YubiKeyError: LocalizedError { 44 | public var errorDescription: String? { 45 | switch self { 46 | case let .connection(message), let .decipher(message), let .other(message), let .selectApplication(message), let .verify(message): 47 | return message 48 | } 49 | } 50 | } 51 | 52 | extension AppError: LocalizedError { 53 | public var errorDescription: String? { 54 | let enumName = String(describing: self) 55 | let localizationKey = "\(enumName.first!.uppercased())\(enumName.dropFirst().prefix { $0 != "(" })Error." 56 | switch self { 57 | case let .readingFile(name), let .repositoryBranchNotFound(name), let .repositoryRemoteBranchNotFound(name): 58 | return localizationKey.localize(name) 59 | case let .pgpPrivateKeyNotFound(keyID), let .pgpPublicKeyNotFound(keyID): 60 | return localizationKey.localize(keyID) 61 | case let .yubiKey(error): 62 | return error.errorDescription 63 | case let .other(message): 64 | return message.localize() 65 | default: 66 | return localizationKey.localize() 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /passKit/Helpers/AppKeychain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppKeychain.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 25.06.19. 6 | // Copyright © 2019 Bob Sun. All rights reserved. 7 | // 8 | 9 | import KeychainAccess 10 | 11 | public class AppKeychain: KeyStore { 12 | public static let shared = AppKeychain() 13 | 14 | private let keychain = Keychain(service: Globals.bundleIdentifier, accessGroup: Globals.groupIdentifier) 15 | .accessibility(.whenUnlockedThisDeviceOnly) 16 | .synchronizable(false) 17 | 18 | public func add(string: String?, for key: String) { 19 | keychain[key] = string 20 | } 21 | 22 | public func contains(key: String) -> Bool { 23 | (try? keychain.contains(key)) ?? false 24 | } 25 | 26 | public func get(for key: String) -> String? { 27 | try? keychain.getString(key) 28 | } 29 | 30 | public func removeContent(for key: String) { 31 | try? keychain.remove(key) 32 | } 33 | 34 | public func removeAllContent() { 35 | try? keychain.removeAll() 36 | } 37 | 38 | public func removeAllContent(withPrefix prefix: String) { 39 | keychain.allKeys() 40 | .filter { $0.hasPrefix(prefix) } 41 | .forEach { try? keychain.remove($0) } 42 | } 43 | 44 | public static func getPGPKeyPassphraseKey(keyID: String) -> String { 45 | Globals.pgpKeyPassphrase + "-" + keyID 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /passKit/Helpers/Colors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Colors.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 01.10.19. 6 | // Copyright © 2019 Bob Sun. All rights reserved. 7 | // 8 | 9 | public enum Colors { 10 | public static let label: UIColor = .label 11 | 12 | public static let secondaryLabel: UIColor = .secondaryLabel 13 | 14 | public static let systemBackground: UIColor = .systemBackground 15 | 16 | public static let secondarySystemBackground: UIColor = .secondarySystemBackground 17 | 18 | public static let systemRed: UIColor = .systemRed 19 | 20 | public static let systemBlue: UIColor = .systemBlue 21 | } 22 | -------------------------------------------------------------------------------- /passKit/Helpers/CryptographicKeys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CryptographicKeys.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 29.06.19. 6 | // Copyright © 2019 Bob Sun. All rights reserved. 7 | // 8 | 9 | public protocol CryptographicKey { 10 | func getKeychainKey() -> String 11 | func getFileSharingPath() -> String 12 | } 13 | 14 | public enum PGPKey: CryptographicKey { 15 | case PUBLIC 16 | case PRIVATE 17 | 18 | public func getKeychainKey() -> String { 19 | switch self { 20 | case .PUBLIC: 21 | return "pgpPublicKey" 22 | case .PRIVATE: 23 | return "pgpPrivateKey" 24 | } 25 | } 26 | 27 | public func getFileSharingPath() -> String { 28 | switch self { 29 | case .PUBLIC: 30 | return Globals.iTunesFileSharingPGPPublic 31 | case .PRIVATE: 32 | return Globals.iTunesFileSharingPGPPrivate 33 | } 34 | } 35 | } 36 | 37 | public enum SSHKey: CryptographicKey { 38 | case PRIVATE 39 | 40 | public func getKeychainKey() -> String { 41 | switch self { 42 | case .PRIVATE: 43 | return "sshPrivateKey" 44 | } 45 | } 46 | 47 | public func getFileSharingPath() -> String { 48 | switch self { 49 | case .PRIVATE: 50 | return Globals.iTunesFileSharingSSHPrivate 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /passKit/Helpers/KeyFileManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyFileManager.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 29.06.19. 6 | // Copyright © 2019 Bob Sun. All rights reserved. 7 | // 8 | 9 | public class KeyFileManager { 10 | public typealias KeyHandler = (String, String) -> Void 11 | 12 | public static let PublicPGP = KeyFileManager(keyType: PGPKey.PUBLIC) 13 | public static let PrivatePGP = KeyFileManager(keyType: PGPKey.PRIVATE) 14 | public static let PrivateSSH = KeyFileManager(keyType: SSHKey.PRIVATE) 15 | 16 | private let keyType: CryptographicKey 17 | private let keyPath: String 18 | private let keyHandler: KeyHandler 19 | 20 | private convenience init(keyType: CryptographicKey) { 21 | self.init(keyType: keyType, keyPath: keyType.getFileSharingPath()) 22 | } 23 | 24 | public init(keyType: CryptographicKey, keyPath: String, keyHandler: @escaping KeyHandler = AppKeychain.shared.add) { 25 | self.keyType = keyType 26 | self.keyPath = keyPath 27 | self.keyHandler = keyHandler 28 | } 29 | 30 | public func importKeyFromFileSharing() throws { 31 | let keyFileContent = try String(contentsOfFile: keyPath, encoding: .ascii) 32 | try importKey(from: keyFileContent) 33 | try FileManager.default.removeItem(atPath: keyPath) 34 | } 35 | 36 | public func importKey(from string: String) throws { 37 | guard string.unicodeScalars.allSatisfy(\.isASCII) else { 38 | throw AppError.encoding 39 | } 40 | keyHandler(string, keyType.getKeychainKey()) 41 | } 42 | 43 | public func importKey(from url: URL) throws { 44 | try importKey(from: String(contentsOf: url, encoding: .ascii)) 45 | } 46 | 47 | public func doesKeyFileExist() -> Bool { 48 | FileManager.default.fileExists(atPath: keyPath) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /passKit/Helpers/KeyStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyStore.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 20.07.19. 6 | // Copyright © 2019 Bob Sun. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol KeyStore { 12 | func add(string: String?, for key: String) 13 | func contains(key: String) -> Bool 14 | func get(for key: String) -> String? 15 | func removeContent(for key: String) 16 | func removeAllContent() 17 | } 18 | -------------------------------------------------------------------------------- /passKit/Helpers/NotificationCenterDispatcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationCenterDispatcher.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 29.09.21. 6 | // Copyright © 2021 Bob Sun. All rights reserved. 7 | // 8 | 9 | public class NotificationCenterDispatcher: NSObject, UNUserNotificationCenterDelegate { 10 | public static let shared = NotificationCenterDispatcher() 11 | 12 | public func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { 13 | if response.actionIdentifier == Globals.otpNotificationCopyAction { 14 | if let otp = response.notification.request.content.userInfo["otp"] as? String { 15 | UIPasteboard.general.string = otp 16 | } 17 | } 18 | completionHandler() 19 | } 20 | 21 | public static func showOTPNotification(password: Password) { 22 | guard let otp = password.currentOtp else { 23 | return 24 | } 25 | let notificationCenter = UNUserNotificationCenter.current() 26 | notificationCenter.getNotificationSettings { state in 27 | guard state.authorizationStatus == .authorized else { 28 | return 29 | } 30 | let content = UNMutableNotificationContent() 31 | content.title = "OTPForPassword".localize(password.name) 32 | if Defaults.autoCopyOTP { 33 | content.body = "OTPHasBeenCopied".localize() 34 | UIPasteboard.general.string = otp 35 | } else { 36 | content.body = otp 37 | content.categoryIdentifier = Globals.otpNotificationCategory 38 | content.userInfo = [ 39 | "path": password.namePath, 40 | "otp": otp, 41 | ] 42 | } 43 | let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) 44 | let request = UNNotificationRequest(identifier: Globals.otpNotification, content: content, trigger: trigger) 45 | notificationCenter.add(request) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /passKit/Helpers/NotificationNames.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationNames.swift 3 | // passKit 4 | // 5 | // Created by Yishi Lin on 17/3/17. 6 | // Copyright © 2017 Yishi Lin, Bob Sun. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension Notification.Name { 12 | static let passwordStoreUpdated = Notification.Name("passwordStoreUpdated") 13 | static let passwordStoreErased = Notification.Name("passwordStoreErased") 14 | static let passwordStoreChangeDiscarded = Notification.Name("passwordStoreChangeDiscarded") 15 | static let passwordStoreSyncSucceeded = Notification.Name("passwordStoreSyncSucceeded") 16 | static let passwordSearch = Notification.Name("passwordSearch") 17 | 18 | static let passwordDisplaySettingChanged = Notification.Name("passwordDisplaySettingChanged") 19 | static let passwordDetailDisplaySettingChanged = Notification.Name("passwordDetailDisplaySettingChanged") 20 | } 21 | -------------------------------------------------------------------------------- /passKit/Helpers/SearchBarScope.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchBarScope.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 05.03.19. 6 | // Copyright © 2019 Bob Sun. All rights reserved. 7 | // 8 | 9 | public enum SearchBarScope: Int { 10 | case current 11 | case all 12 | 13 | public var localizedName: String { 14 | switch self { 15 | case .current: 16 | return "Current".localize() 17 | case .all: 18 | return "All".localize() 19 | } 20 | } 21 | } 22 | 23 | extension SearchBarScope: CaseIterable {} 24 | -------------------------------------------------------------------------------- /passKit/Helpers/YubiKeyAPDU.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YubiKeyAPDU.swift 3 | // passKit 4 | // 5 | // Copyright © 2022 Bob Sun. All rights reserved. 6 | // 7 | 8 | import YubiKit 9 | 10 | public enum YubiKeyAPDU { 11 | public static func selectOpenPGPApplication() -> YKFSelectApplicationAPDU { 12 | YKFSelectApplicationAPDU(data: Data([0xD2, 0x76, 0x00, 0x01, 0x24, 0x01]))! 13 | } 14 | 15 | public static func verify(password: String) -> YKFAPDU { 16 | YKFAPDU(cla: 0x00, ins: 0x20, p1: 0x00, p2: 0x82, data: Data(password.utf8), type: .extended)! 17 | } 18 | 19 | public static func decipherExtended(data: Data) -> [YKFAPDU] { 20 | let apdu = YKFAPDU(cla: 0x00, ins: 0x2A, p1: 0x80, p2: 0x86, data: data, type: .extended)! 21 | return [apdu] 22 | } 23 | 24 | public static func decipherChained(data: Data) -> [YKFAPDU] { 25 | var result: [YKFAPDU] = [] 26 | let chunks = chunk(data: data) 27 | 28 | for chunk in chunks.dropLast() { 29 | var apdu: [UInt8] = [] 30 | apdu += [0x10] // CLA (command is not the last command of a chain) 31 | apdu += [0x2A, 0x80, 0x86] // INS, P1, P2: PSO.DECIPHER 32 | apdu += withUnsafeBytes(of: UInt8(chunk.count).bigEndian, Array.init) 33 | apdu += chunk 34 | result += [YKFAPDU(data: Data(apdu))!] 35 | } 36 | 37 | var apdu: [UInt8] = [] 38 | apdu += [0x00] // CLA (last or only command of a chain) 39 | apdu += [0x2A, 0x80, 0x86] // INS, P1, P2: PSO.DECIPHER 40 | apdu += withUnsafeBytes(of: UInt8(chunks.last!.count).bigEndian, Array.init) 41 | apdu += chunks.last! 42 | apdu += [0x00] // Le 43 | result += [YKFAPDU(data: Data(apdu))!] 44 | 45 | return result 46 | } 47 | 48 | public static func getApplicationRelatedData() -> YKFAPDU { 49 | YKFAPDU(cla: 0x00, ins: 0xCA, p1: 0x00, p2: 0x6E, data: Data(), type: .short)! 50 | } 51 | 52 | static func chunk(data: Data) -> [[UInt8]] { 53 | // starts with 00 padding 54 | let padded: [UInt8] = [0x00] + data 55 | let MAX_SIZE = 254 56 | 57 | return stride(from: 0, to: padded.count, by: MAX_SIZE).map { 58 | Array(padded[$0 ..< Swift.min($0 + MAX_SIZE, padded.count)]) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /passKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | $(PRODUCT_NAME) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | FMWK 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | 0 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /passKit/Models/PasscodeLock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeLock.swift 3 | // passKit 4 | // 5 | // Created by Yishi Lin on 28/1/2018. 6 | // Copyright © 2017 Yishi Lin. All rights reserved. 7 | // 8 | 9 | public class PasscodeLock { 10 | public static let shared = PasscodeLock() 11 | 12 | private let identifier = Globals.bundleIdentifier + "passcode" 13 | 14 | private var passcode: String? { 15 | AppKeychain.shared.get(for: identifier) 16 | } 17 | 18 | /// Constructor used to migrate passcode from SharedDefaults to Keychain 19 | private init() { 20 | if let passcode = Defaults.passcodeKey { 21 | save(passcode: passcode) 22 | Defaults.passcodeKey = nil 23 | } 24 | } 25 | 26 | public var hasPasscode: Bool { 27 | passcode != nil 28 | } 29 | 30 | public func save(passcode: String) { 31 | AppKeychain.shared.add(string: passcode, for: identifier) 32 | } 33 | 34 | public func check(passcode: String) -> Bool { 35 | self.passcode == passcode 36 | } 37 | 38 | public func delete() { 39 | AppKeychain.shared.removeContent(for: identifier) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /passKit/Models/PasswordTableEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordTableEntry.swift 3 | // passKit 4 | // 5 | // Created by Yishi Lin on 2020/2/23. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class PasswordTableEntry: NSObject { 12 | public let passwordEntity: PasswordEntity 13 | @objc public let title: String 14 | public let isDir: Bool 15 | public let isSynced: Bool 16 | public let categoryText: String 17 | 18 | public init(_ entity: PasswordEntity) { 19 | self.passwordEntity = entity 20 | self.title = entity.name 21 | self.isDir = entity.isDir 22 | self.isSynced = entity.isSynced 23 | self.categoryText = entity.dirText 24 | } 25 | 26 | public func matches(_ searchText: String) -> Bool { 27 | Self.match(nameWithCategory: passwordEntity.nameWithDir, searchText: searchText) 28 | } 29 | 30 | public static func match(nameWithCategory: String, searchText: String) -> Bool { 31 | let titleSplit = nameWithCategory.split { !($0.isLetter || $0.isNumber || $0 == ".") } 32 | for str in titleSplit { 33 | if str.localizedCaseInsensitiveContains(searchText) { 34 | return true 35 | } 36 | if searchText.localizedCaseInsensitiveContains(str) { 37 | return true 38 | } 39 | } 40 | 41 | return false 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /passKit/Parser/AdditionField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdditionField.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 30.09.18. 6 | // Copyright © 2018 Bob Sun. All rights reserved. 7 | // 8 | 9 | public struct AdditionField: Hashable { 10 | public let title: String, content: String 11 | 12 | public init(title: String = "", content: String = "") { 13 | self.title = title 14 | self.content = content 15 | } 16 | 17 | var asString: String { 18 | title.isEmpty ? content : title + ": " + content 19 | } 20 | 21 | var asTuple: (String, String) { 22 | (title, content) 23 | } 24 | } 25 | 26 | extension AdditionField { 27 | static func | (left: String, right: AdditionField) -> String { 28 | left | right.asString 29 | } 30 | 31 | static func | (left: AdditionField, right: String) -> String { 32 | left.asString | right 33 | } 34 | 35 | static func | (left: AdditionField, right: AdditionField) -> String { 36 | left.asString | right 37 | } 38 | } 39 | 40 | infix operator =>: MultiplicationPrecedence 41 | 42 | public extension String { 43 | static func => (key: String, value: String) -> AdditionField { 44 | AdditionField(title: key, content: value) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /passKit/Parser/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 16.08.18. 6 | // Copyright © 2018 Bob Sun. All rights reserved. 7 | // 8 | 9 | import OneTimePassword 10 | 11 | public enum Constants { 12 | static let OTP_SECRET = "otp_secret" 13 | static let OTP_TYPE = "otp_type" 14 | static let OTP_ALGORITHM = "otp_algorithm" 15 | static let OTP_PERIOD = "otp_period" 16 | static let OTP_DIGITS = "otp_digits" 17 | static let OTP_COUNTER = "otp_counter" 18 | static let OTP_REPRESENTATION = "otp_representation" 19 | static let OTPAUTH = "otpauth" 20 | 21 | public static let OTP_KEYWORDS = [ 22 | OTP_SECRET, 23 | OTP_TYPE, 24 | OTP_ALGORITHM, 25 | OTP_PERIOD, 26 | OTP_DIGITS, 27 | OTP_COUNTER, 28 | OTP_REPRESENTATION, 29 | OTPAUTH, 30 | ] 31 | 32 | static let TOTP = "totp" 33 | static let HOTP = "hotp" 34 | static let SHA256 = "sha256" 35 | static let SHA512 = "sha512" 36 | static let DEFAULT_DIGITS = 6 37 | static let DEFAULT_PERIOD = 30.0 38 | static let DEFAULT_COUNTER: UInt64? = nil 39 | static let DEFAULT_REPRESENTATION: OneTimePassword.Generator.Representation = .numeric 40 | 41 | static let BLANK = " " 42 | static let MULTILINE_WITH_LINE_BREAK_INDICATOR = "|" 43 | static let MULTILINE_WITH_LINE_BREAK_SEPARATOR = "\n" 44 | static let MULTILINE_WITHOUT_LINE_BREAK_INDICATOR = ">" 45 | static let MULTILINE_WITHOUT_LINE_BREAK_SEPARATOR = BLANK 46 | 47 | static let OTPAUTH_URL_START = "\(OTPAUTH)://" 48 | 49 | public static let PASSWORD_KEYWORD = "password" 50 | public static let USERNAME_KEYWORD = "username" 51 | public static let USER_KEYWORD = "user" 52 | public static let LOGIN_KEYWORD = "login" 53 | public static let URL_KEYWORD = "url" 54 | public static let UNKNOWN = "unknown" 55 | 56 | public static func isOtpRelated(line: String) -> Bool { 57 | let (key, _) = Parser.getKeyValuePair(from: line) 58 | return key != nil && isOtpKeyword(key!) 59 | } 60 | 61 | static func isOtpKeyword(_ keyword: String) -> Bool { 62 | OTP_KEYWORDS.contains(keyword.lowercased()) 63 | } 64 | 65 | static func isUnknown(_ string: String) -> Bool { 66 | let components = string.components(separatedBy: " ") 67 | return components.count == 2 && components[0] == UNKNOWN && UInt(components[1]) != nil 68 | } 69 | 70 | static func unknown(_ number: UInt) -> String { 71 | "\(UNKNOWN) \(number)" 72 | } 73 | 74 | static func getSeparator(breakingLines: Bool) -> String { 75 | breakingLines ? MULTILINE_WITH_LINE_BREAK_SEPARATOR : MULTILINE_WITHOUT_LINE_BREAK_SEPARATOR 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /passKit/Parser/OTPType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OTPType.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 01.12.2018. 6 | // Copyright © 2018 Bob Sun. All rights reserved. 7 | // 8 | 9 | import OneTimePassword 10 | 11 | public enum OTPType: String { 12 | case totp = "TimeBased" 13 | case hotp = "HmacBased" 14 | case none = "None" 15 | 16 | var description: String { 17 | rawValue.localize() 18 | } 19 | 20 | init(token: Token?) { 21 | switch token?.generator.factor { 22 | case .some(.counter): 23 | self = .hotp 24 | case .some(.timer): 25 | self = .totp 26 | default: 27 | self = .none 28 | } 29 | } 30 | 31 | init(name: String?) { 32 | switch name?.lowercased() { 33 | case Constants.HOTP: 34 | self = .hotp 35 | case Constants.TOTP: 36 | self = .totp 37 | default: 38 | self = .none 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /passKit/Parser/PasswordChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordChange.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 01.12.2018. 6 | // Copyright © 2018 Bob Sun. All rights reserved. 7 | // 8 | 9 | enum PasswordChange: Int { 10 | case path = 0x01 11 | case content = 0x02 12 | case none = 0x00 13 | } 14 | -------------------------------------------------------------------------------- /passKit/Passwords/PasswordGeneratorFlavor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordGeneratorFlavor.swift 3 | // passKit 4 | // 5 | // Created by Danny Moesch on 28.11.18. 6 | // Copyright © 2018 Bob Sun. All rights reserved. 7 | // 8 | 9 | public typealias LengthLimits = (min: Int, max: Int) 10 | 11 | public enum PasswordGeneratorFlavor: String { 12 | case random = "Random" 13 | case xkcd = "XKCD" 14 | 15 | public var localized: String { 16 | rawValue.localize() 17 | } 18 | 19 | public var lengthLimits: LengthLimits { 20 | switch self { 21 | case .random: 22 | return (4, 64) 23 | case .xkcd: 24 | return (2, 5) 25 | } 26 | } 27 | } 28 | 29 | extension PasswordGeneratorFlavor: CaseIterable {} 30 | -------------------------------------------------------------------------------- /passKit/Protocols/AlertPresenting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertPresenting.swift 3 | // pass 4 | // 5 | // Copyright © 2022 Bob Sun. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | public typealias AlertAction = (UIAlertAction) -> Void 11 | 12 | public protocol AlertPresenting { 13 | func presentAlert(title: String, message: String) 14 | func presentFailureAlert(title: String?, message: String, action: AlertAction?) 15 | func presentAlertWithAction(title: String, message: String, action: AlertAction?) 16 | } 17 | 18 | public extension AlertPresenting where Self: UIViewController { 19 | func presentAlert(title: String, message: String) { 20 | presentAlert( 21 | title: title, 22 | message: message, 23 | actions: [UIAlertAction(title: "OK", style: .cancel, handler: nil)] 24 | ) 25 | } 26 | 27 | // swiftlint:disable:next function_default_parameter_at_end 28 | func presentFailureAlert(title: String? = nil, message: String, action: AlertAction? = nil) { 29 | let title = title ?? "Error" 30 | presentAlert( 31 | title: title, 32 | message: message, 33 | actions: [UIAlertAction(title: "OK", style: .cancel, handler: action)] 34 | ) 35 | } 36 | 37 | func presentAlertWithAction(title: String, message: String, action: AlertAction?) { 38 | presentAlert( 39 | title: title, 40 | message: message, 41 | actions: [ 42 | UIAlertAction(title: "Yes", style: .default, handler: action), 43 | UIAlertAction(title: "No", style: .cancel, handler: nil), 44 | ] 45 | ) 46 | } 47 | 48 | private func presentAlert(title: String, message: String, actions: [UIAlertAction] = []) { 49 | let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) 50 | actions.forEach { action in 51 | alertController.addAction(action) 52 | } 53 | present(alertController, animated: true, completion: nil) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /passKit/pass.xcdatamodeld/pass.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /passKit/passKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // passKit.h 3 | // passKit 4 | // 5 | // Created by Yishi Lin on 11/6/17. 6 | // Copyright © 2017年 Bob Sun. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for passKit. 12 | FOUNDATION_EXPORT double passKitVersionNumber; 13 | 14 | //! Project version string for passKit. 15 | FOUNDATION_EXPORT const unsigned char passKitVersionString[]; 16 | -------------------------------------------------------------------------------- /passKitTests/CoreData/CoreDataTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataTestCase.swift 3 | // pass 4 | // 5 | // Created by Mingshen Sun on 1/4/25. 6 | // Copyright © 2025 Bob Sun. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | import Foundation 11 | import XCTest 12 | 13 | @testable import passKit 14 | 15 | // swiftlint:disable:next final_test_case 16 | class CoreDataTestCase: XCTestCase { 17 | // swiftlint:disable:next test_case_accessibility 18 | private(set) var controller: PersistenceController! 19 | 20 | override func setUpWithError() throws { 21 | try super.setUpWithError() 22 | 23 | controller = PersistenceController(isUnitTest: true) 24 | } 25 | 26 | override func tearDown() { 27 | super.tearDown() 28 | controller = nil 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /passKitTests/Extensions/Array+SlicesTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+SlicesTest.swift 3 | // passKitTests 4 | // 5 | // Created by Danny Moesch on 28.02.20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | @testable import passKit 12 | 13 | final class ArraySlicesTest: XCTestCase { 14 | func testZeroCount() { 15 | XCTAssertEqual([1, 2, 3].slices(count: 0), []) 16 | } 17 | 18 | func testEmptyArray() { 19 | XCTAssertEqual(([] as [String]).slices(count: 4), [[], [], [], []]) 20 | } 21 | 22 | func testSlices() { 23 | XCTAssertEqual([1, 2, 3].slices(count: 3), [[1], [2], [3]]) 24 | XCTAssertEqual([1, 2, 3, 4].slices(count: 3), [[1], [2], [3, 4]]) 25 | XCTAssertEqual([1, 2, 3, 4].slices(count: 2), [[1, 2], [3, 4]]) 26 | XCTAssertEqual([1, 2, 3, 4, 5].slices(count: 2), [[1, 2], [3, 4, 5]]) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /passKitTests/Extensions/String+UtilitiesTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+UtilitiesTest.swift 3 | // passKitTests 4 | // 5 | // Created by Danny Moesch on 30.09.18. 6 | // Copyright © 2018 Bob Sun. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | @testable import passKit 12 | 13 | final class StringUtilitiesTest: XCTestCase { 14 | func testTrimmed() { 15 | [ 16 | (" ", ""), 17 | (" \n\t\r", ""), 18 | ("\t a \n b \t c \r d \n ", "a \n b \t c \r d"), 19 | ].forEach { untrimmed, trimmed in 20 | XCTAssertEqual(untrimmed.trimmed, trimmed) 21 | } 22 | } 23 | 24 | func testStringByAddingPercentEncodingForRFC3986() { 25 | [ 26 | ("!#$&'()*+,/:;=?@[]^", "%21%23%24%26%27%28%29%2A%2B%2C/%3A%3B%3D%3F%40%5B%5D%5E"), 27 | ("-._~/", "-._~/"), 28 | ("A*b!c", "A%2Ab%21c"), 29 | ].forEach { unencoded, encoded in 30 | XCTAssertEqual(unencoded.stringByAddingPercentEncodingForRFC3986(), encoded) 31 | } 32 | } 33 | 34 | func testConcatenateAsLines() { 35 | [ 36 | ("a" | "b", "a\nb"), 37 | ("" | "b", "\nb"), 38 | ("a" | "", "a"), 39 | ("" | "", ""), 40 | ].forEach { concatenated, result in 41 | XCTAssertEqual(concatenated, result) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /passKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 0 21 | 22 | 23 | -------------------------------------------------------------------------------- /passKitTests/Models/PasswordStoreTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordStoreTest.swift 3 | // passKitTests 4 | // 5 | // Created by Mingshen Sun on 13/4/2020. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ObjectiveGit 11 | import XCTest 12 | 13 | @testable import passKit 14 | 15 | final class PasswordStoreTest: XCTestCase { 16 | private let remoteRepoURL = URL(string: "https://github.com/mssun/passforios-password-store.git")! 17 | 18 | func testCloneAndDecryptMultiKeys() throws { 19 | let url = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/") 20 | 21 | Defaults.isEnableGPGIDOn = true 22 | let passwordStore = PasswordStore(url: url) 23 | try passwordStore.cloneRepository(remoteRepoURL: remoteRepoURL, branchName: "master") 24 | expectation(for: NSPredicate { _, _ in FileManager.default.fileExists(atPath: url.path) }, evaluatedWith: nil) 25 | waitForExpectations(timeout: 3, handler: nil) 26 | 27 | [ 28 | ("work/github.com", "4712286271220DB299883EA7062E678DA1024DAE"), 29 | ("personal/github.com", "787EAE1A5FA3E749AA34CC6AA0645EBED862027E"), 30 | ].forEach { path, id in 31 | let keyID = findGPGID(from: url.appendingPathComponent(path)) 32 | XCTAssertEqual(keyID, id) 33 | } 34 | 35 | let keychain = AppKeychain.shared 36 | try KeyFileManager(keyType: PGPKey.PUBLIC, keyPath: "", keyHandler: keychain.add).importKey(from: RSA2048_RSA4096.publicKeys) 37 | try KeyFileManager(keyType: PGPKey.PRIVATE, keyPath: "", keyHandler: keychain.add).importKey(from: RSA2048_RSA4096.privateKeys) 38 | try PGPAgent.shared.initKeys() 39 | 40 | let personal = try decrypt(passwordStore: passwordStore, path: "personal/github.com.gpg", passphrase: "passforios") 41 | XCTAssertEqual(personal.plainText, "passwordforpersonal\n") 42 | 43 | let work = try decrypt(passwordStore: passwordStore, path: "work/github.com.gpg", passphrase: "passforios") 44 | XCTAssertEqual(work.plainText, "passwordforwork\n") 45 | 46 | let testPassword = Password(name: "test", path: "test.gpg", plainText: "testpassword") 47 | let testPasswordEntity = try passwordStore.add(password: testPassword)! 48 | let testPasswordPlain = try passwordStore.decrypt(passwordEntity: testPasswordEntity, requestPGPKeyPassphrase: requestPGPKeyPassphrase) 49 | XCTAssertEqual(testPasswordPlain.plainText, "testpassword") 50 | 51 | passwordStore.erase() 52 | Defaults.isEnableGPGIDOn = false 53 | } 54 | 55 | private func decrypt(passwordStore: PasswordStore, path: String, passphrase _: String) throws -> Password { 56 | let entity = passwordStore.fetchPasswordEntity(with: path)! 57 | return try passwordStore.decrypt(passwordEntity: entity, requestPGPKeyPassphrase: requestPGPKeyPassphrase) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /passKitTests/Models/PasswordTableEntryTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordTableEntryTest.swift 3 | // passKitTests 4 | // 5 | // Created by Yishi Lin on 2020/2/23. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | @testable import passKit 12 | 13 | final class PasswordTableEntryTest: XCTestCase { 14 | func testExample() { 15 | let nameWithCategoryList = [ 16 | "github", 17 | "github.com", 18 | "www.github.com", 19 | "personal/github", 20 | "personal/github.com", 21 | "personal/www.github.com", 22 | "github/personal", 23 | "github.com/personal", 24 | "www.github.com/personal", 25 | "github (personal)", 26 | ] 27 | let searchTextList1 = [ 28 | "github.com", 29 | "www.github.com", 30 | ] 31 | let searchTextList2 = [ 32 | "xx.com", 33 | "www.xx.com", 34 | ] 35 | 36 | for nameWithCategory in nameWithCategoryList { 37 | for searchText in searchTextList1 { 38 | XCTAssertTrue(PasswordTableEntry.match(nameWithCategory: nameWithCategory, searchText: searchText)) 39 | } 40 | for searchText in searchTextList2 { 41 | XCTAssertFalse(PasswordTableEntry.match(nameWithCategory: nameWithCategory, searchText: searchText)) 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /passKitTests/Parser/AdditionFieldTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdditionFieldTest.swift 3 | // passKitTests 4 | // 5 | // Created by Danny Moesch on 30.09.18. 6 | // Copyright © 2018 Bob Sun. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | @testable import passKit 12 | 13 | final class AdditionFieldTest: XCTestCase { 14 | func testAdditionField() { 15 | let field1 = AdditionField(title: "key", content: "value") 16 | let field2 = AdditionField(title: "no content") 17 | let field3 = AdditionField(content: "no title") 18 | 19 | XCTAssertEqual(field1.asString, "key: value") 20 | XCTAssertEqual(field2.asString, "no content: ") 21 | XCTAssertEqual(field3.asString, "no title") 22 | 23 | XCTAssert(field1.asTuple == ("key", "value")) 24 | XCTAssert(field2.asTuple == ("no content", "")) 25 | XCTAssert(field3.asTuple == ("", "no title")) 26 | } 27 | 28 | func testAdditionFieldEquals() { 29 | XCTAssertEqual("key" => "value", "key" => "value") 30 | XCTAssertNotEqual("key" => "value", "key" => "some other value") 31 | } 32 | 33 | func testInfixAdditionFieldInitialization() { 34 | XCTAssertEqual("key" => "value", AdditionField(title: "key", content: "value")) 35 | } 36 | 37 | func testAdditionFieldOperators() { 38 | let field1 = "key" => "value" 39 | let field2 = "some other key" => "some other value" 40 | let field3 = "" => "no title" 41 | 42 | XCTAssertEqual("start" | field1, "start\nkey: value") 43 | XCTAssertEqual("" | field1, "\nkey: value") 44 | XCTAssertEqual(field1 | "end", "key: value\nend") 45 | XCTAssertEqual(field1 | "", "key: value") 46 | XCTAssertEqual("start" | field1 | field2, "start\nkey: value\nsome other key: some other value") 47 | XCTAssertEqual(field1 | field2 | "end", "key: value\nsome other key: some other value\nend") 48 | XCTAssertEqual(field1 | field2 | field3, "key: value\nsome other key: some other value\nno title") 49 | XCTAssertEqual("check" => "for right" | "operator" => "precedence", "check: for right\noperator: precedence") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /passKitTests/Parser/ConstantsTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConstantsTest.swift 3 | // passKitTests 4 | // 5 | // Created by Danny Moesch on 30.09.18. 6 | // Copyright © 2018 Bob Sun. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | @testable import passKit 12 | 13 | final class ConstantsTest: XCTestCase { 14 | func testIsOtpRelated() { 15 | XCTAssert(Constants.isOtpRelated(line: "otpauth://something")) 16 | XCTAssert(Constants.isOtpRelated(line: "otp_algorithm: algorithm")) 17 | XCTAssertFalse(Constants.isOtpRelated(line: "otp: something")) 18 | XCTAssertFalse(Constants.isOtpRelated(line: "otp")) 19 | } 20 | 21 | func testIsOtpKeyword() { 22 | XCTAssert(Constants.isOtpKeyword("otpauth")) 23 | XCTAssert(Constants.isOtpKeyword("oTP_DigITS")) 24 | XCTAssertFalse(Constants.isOtpKeyword("otp")) 25 | XCTAssertFalse(Constants.isOtpKeyword("no keyword")) 26 | } 27 | 28 | func testIsUnknown() { 29 | XCTAssert(Constants.isUnknown("unknown 1")) 30 | XCTAssert(Constants.isUnknown("unknown 435")) 31 | XCTAssertFalse(Constants.isUnknown("otp")) 32 | XCTAssertFalse(Constants.isUnknown("unknown ")) 33 | XCTAssertFalse(Constants.isUnknown("unknown something")) 34 | XCTAssertFalse(Constants.isUnknown("unknown 123 something")) 35 | XCTAssertFalse(Constants.isUnknown("Unknown 1")) 36 | } 37 | 38 | func testUnknown() { 39 | XCTAssertEqual(Constants.unknown(0), "unknown 0") 40 | XCTAssertEqual(Constants.unknown(10), "unknown 10") 41 | } 42 | 43 | func testGetSeparator() { 44 | XCTAssertEqual(Constants.getSeparator(breakingLines: true), "\n") 45 | XCTAssertEqual(Constants.getSeparator(breakingLines: false), " ") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /passKitTests/Parser/OTPTypeTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OTPTypeTest.swift 3 | // passKitTests 4 | // 5 | // Created by Danny Moesch on 01.12.18. 6 | // Copyright © 2018 Bob Sun. All rights reserved. 7 | // 8 | 9 | import OneTimePassword 10 | import XCTest 11 | 12 | @testable import passKit 13 | 14 | final class OTPTypeTest: XCTestCase { 15 | func testInitFromToken() { 16 | let secret = Data("secret".utf8) 17 | 18 | let totpGenerator = Generator(factor: .timer(period: 30.0), secret: secret, algorithm: .sha1, digits: 6)! 19 | let totpToken = Token(name: "", issuer: "", generator: totpGenerator) 20 | XCTAssertEqual(OTPType(token: totpToken), .totp) 21 | 22 | let hotpGenerator = Generator(factor: .counter(4), secret: secret, algorithm: .sha1, digits: 6)! 23 | let hotpToken = Token(name: "", issuer: "", generator: hotpGenerator) 24 | XCTAssertEqual(OTPType(token: hotpToken), .hotp) 25 | 26 | XCTAssertEqual(OTPType(token: nil), .none) 27 | } 28 | 29 | func testInitFromString() { 30 | XCTAssertEqual(OTPType(name: "totp"), .totp) 31 | XCTAssertEqual(OTPType(name: "tOtP"), .totp) 32 | XCTAssertEqual(OTPType(name: "hotp"), .hotp) 33 | XCTAssertEqual(OTPType(name: "HoTp"), .hotp) 34 | XCTAssertEqual(OTPType(name: nil), .none) 35 | XCTAssertEqual(OTPType(name: ""), .none) 36 | XCTAssertEqual(OTPType(name: "something"), .none) 37 | } 38 | 39 | func testDescription() { 40 | XCTAssertEqual(OTPType(name: "totp").description, "TimeBased".localize()) 41 | XCTAssertEqual(OTPType(name: "hotp").description, "HmacBased".localize()) 42 | XCTAssertEqual(OTPType(name: nil).description, "None".localize()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /passKitTests/Passwords/PasswordGeneratorFlavorTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordGeneratorFlavorTest.swift 3 | // passKitTests 4 | // 5 | // Created by Danny Moesch on 28.11.18. 6 | // Copyright © 2018 Bob Sun. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | @testable import passKit 12 | 13 | final class PasswordGeneratorFlavorTest: XCTestCase { 14 | func testLengthLimits() { 15 | // Ensure properly chosen length limits. So this check no longer needs to be performed in the code. 16 | PasswordGeneratorFlavor.allCases.map(\.lengthLimits).forEach { 17 | XCTAssertLessThanOrEqual($0.min, $0.max) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /passKitTests/Testbase/DictBasedKeychain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictBasedKeychain.swift 3 | // passKitTests 4 | // 5 | // Created by Danny Moesch on 20.07.19. 6 | // Copyright © 2019 Bob Sun. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import passKit 11 | 12 | class DictBasedKeychain: KeyStore { 13 | private var store: [String: Any] = [:] 14 | 15 | func add(string: String?, for key: String) { 16 | store[key] = string 17 | } 18 | 19 | func contains(key: String) -> Bool { 20 | store[key] != nil 21 | } 22 | 23 | func get(for key: String) -> String? { 24 | store[key] as? String 25 | } 26 | 27 | func removeContent(for key: String) { 28 | store.removeValue(forKey: key) 29 | } 30 | 31 | func removeAllContent() { 32 | store.removeAll() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /passKitTests/Testbase/TestBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestBase.swift 3 | // passKitTests 4 | // 5 | // Created by Danny Moesch on 08.12.18. 6 | // Copyright © 2018 Bob Sun. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | @testable import passKit 12 | 13 | let PASSWORD_PATH = "/path/to/password" 14 | let PASSWORD_STRING = "abcd1234" 15 | let TOTP_URL = "otpauth://totp/email@email.com?secret=abcd1234" 16 | let STEAM_TOTP_URL = "otpauth://totp/username?secret=12345678901234567890&issuer=Steam&algorithm=SHA1&digits=5&period=30&representation=steamguard" 17 | let HOTP_URL = "otpauth://hotp/email@email.com?secret=abcd1234" 18 | 19 | let FIELD = "key" => "value" 20 | let SECURE_URL_FIELD = "url" => "https://secure.com" 21 | let INSECURE_URL_FIELD = "url" => "http://insecure.com" 22 | let LOGIN_FIELD = "login" => "login name" 23 | let USERNAME_FIELD = "username" => "微 分 方 程" 24 | let USER_FIELD = "user" => "积 分 方 程" 25 | let NOTE_FIELD = "note" => "A NOTE" 26 | let HINT_FIELD = "some hints" => "äöüß // €³ %% −° && @²` | [{\\}],.<>" 27 | let TOTP_URL_FIELD = "otpauth" => "//totp/email@email.com?secret=abcd1234" 28 | 29 | let MULTILINE_BLOCK_START = "multiline block" => "|" 30 | let MULTILINE_LINE_START = "multiline line" => ">" 31 | 32 | func getPasswordObjectWith(content: String, path: String? = nil) -> Password { 33 | Password(name: "password", path: path ?? PASSWORD_PATH, plainText: content) 34 | } 35 | 36 | func assertDefaults( 37 | in password: Password, 38 | with passwordString: String, 39 | and additions: String, 40 | at file: StaticString = #file, 41 | line: UInt = #line 42 | ) { 43 | let fileContent = (passwordString | additions).data(using: .utf8) 44 | XCTAssertEqual(password.password, passwordString, "Actual passwords do not match.", file: file, line: line) 45 | XCTAssertEqual(password.plainData, fileContent, "Plain data are not equal.", file: file, line: line) 46 | XCTAssertEqual(password.additionsPlainText, additions, "Plain texts are not equal.", file: file, line: line) 47 | XCTAssertEqual(password.numberOfUnknowns, 0, "Number of unknowns is not 0.", file: file, line: line) 48 | XCTAssertEqual(password.numberOfOtpRelated, 0, "Number of OTP related fields is not 0.", file: file, line: line) 49 | XCTAssertEqual(password.otpType, .none, "OTP type is not .none.", file: file, line: line) 50 | } 51 | 52 | infix operator ∈: AdditionPrecedence 53 | infix operator ∉: AdditionPrecedence 54 | 55 | extension AdditionField { 56 | static func ∈ (field: AdditionField, password: Password) -> Bool { // swiftlint:disable:this identifier_name 57 | password.getFilteredAdditions().contains(field) 58 | } 59 | 60 | static func ∉ (field: AdditionField, password: Password) -> Bool { // swiftlint:disable:this identifier_name 61 | !(field ∈ password) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /passShortcuts/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | $(PRODUCT_NAME) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | 0 23 | NSExtension 24 | 25 | NSExtensionAttributes 26 | 27 | IntentsRestrictedWhileLocked 28 | 29 | SyncRepositoryIntent 30 | 31 | IntentsRestrictedWhileProtectedDataUnavailable 32 | 33 | SyncRepositoryIntent 34 | 35 | IntentsSupported 36 | 37 | SyncRepositoryIntent 38 | 39 | 40 | NSExtensionPointIdentifier 41 | com.apple.intents-service 42 | NSExtensionPrincipalClass 43 | $(PRODUCT_MODULE_NAME).IntentHandler 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /passShortcuts/IntentHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntentHandler.swift 3 | // passShortcuts 4 | // 5 | // Created by Danny Moesch on 03.03.20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import Intents 10 | import passKit 11 | 12 | class IntentHandler: INExtension { 13 | override func handler(for intent: INIntent) -> Any { 14 | guard intent is SyncRepositoryIntent else { 15 | fatalError("Unhandled intent type \(intent).") 16 | } 17 | return SyncRepositoryIntentHandler() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /passShortcuts/SyncRepositoryIntentHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyncRepositoryIntentHandler.swift 3 | // passShortcuts 4 | // 5 | // Created by Danny Moesch on 03.03.20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import Intents 10 | import passKit 11 | 12 | public class SyncRepositoryIntentHandler: NSObject, SyncRepositoryIntentHandling { 13 | private let passwordStore = PasswordStore.shared 14 | private let keychain = AppKeychain.shared 15 | 16 | private var gitCredential: GitCredential { 17 | GitCredential.from( 18 | authenticationMethod: Defaults.gitAuthenticationMethod, 19 | userName: Defaults.gitUsername, 20 | keyStore: keychain 21 | ) 22 | } 23 | 24 | public func handle(intent _: SyncRepositoryIntent, completion: @escaping (SyncRepositoryIntentResponse) -> Void) { 25 | guard passwordStore.repositoryExists() else { 26 | completion(SyncRepositoryIntentResponse(code: .noRepository, userActivity: nil)) 27 | return 28 | } 29 | guard isPasswordRemembered else { 30 | completion(SyncRepositoryIntentResponse(code: .noPassphrase, userActivity: nil)) 31 | return 32 | } 33 | do { 34 | try passwordStore.pullRepository(options: gitCredential.getCredentialOptions()) 35 | } catch { 36 | completion(SyncRepositoryIntentResponse(code: .pullFailed, userActivity: nil)) 37 | return 38 | } 39 | if passwordStore.numberOfLocalCommits > 0 { 40 | do { 41 | try passwordStore.pushRepository(options: gitCredential.getCredentialOptions()) 42 | } catch { 43 | completion(SyncRepositoryIntentResponse(code: .pushFailed, userActivity: nil)) 44 | return 45 | } 46 | } 47 | completion(SyncRepositoryIntentResponse(code: .success, userActivity: nil)) 48 | } 49 | 50 | private var isPasswordRemembered: Bool { 51 | let authenticationMethod = Defaults.gitAuthenticationMethod 52 | return authenticationMethod == .password && keychain.contains(key: Globals.gitPassword) 53 | || authenticationMethod == .key && keychain.contains(key: Globals.gitSSHPrivateKeyPassphrase) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /passShortcuts/passBetaShortcuts.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.me.mssun.passforiosbeta 8 | 9 | keychain-access-groups 10 | 11 | $(AppIdentifierPrefix)group.me.mssun.passforiosbeta 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /passShortcuts/passShortcuts.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.me.mssun.passforios 8 | 9 | keychain-access-groups 10 | 11 | $(AppIdentifierPrefix)group.me.mssun.passforios 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /passTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 0 21 | 22 | 23 | -------------------------------------------------------------------------------- /passTests/Models/QRKeyScannerTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QRKeyScannerTest.swift 3 | // passTests 4 | // 5 | // Created by Danny Moesch on 21.08.20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | @testable import Pass 12 | 13 | final class QRKeyScannerTest: XCTestCase { 14 | private let header = "-----BEGIN PGP PUBLIC KEY BLOCK-----" 15 | private let body = "key body" 16 | private let footer = "-----END PGP PUBLIC KEY BLOCK-----" 17 | private let footerWithNewline = "-----END PGP PUBLIC KEY BLOCK-----\n" 18 | private let privateHeader = "-----BEGIN PGP PRIVATE KEY BLOCK-----" 19 | 20 | private var scanner = QRKeyScanner(keyType: .pgpPublic) 21 | 22 | func testAddHeaderTwice() { 23 | XCTAssertEqual(scanner.add(segment: header), .scanned(1)) 24 | XCTAssertEqual(scanner.add(segment: header), .scanned(1)) 25 | XCTAssertEqual(scanner.scannedKey, header) 26 | } 27 | 28 | func testAddBodyTwice() { 29 | XCTAssertEqual(scanner.add(segment: header), .scanned(1)) 30 | XCTAssertEqual(scanner.add(segment: body), .scanned(2)) 31 | XCTAssertEqual(scanner.add(segment: body), .scanned(2)) 32 | XCTAssertEqual(scanner.scannedKey, header + body) 33 | } 34 | 35 | func testAddCompleteBlock() { 36 | XCTAssertEqual(scanner.add(segment: header), .scanned(1)) 37 | XCTAssertEqual(scanner.add(segment: footer), .completed) 38 | XCTAssertEqual(scanner.scannedKey, header + footer) 39 | } 40 | 41 | func testCounterKeyType() { 42 | XCTAssertEqual(scanner.add(segment: privateHeader), .wrongKeyType(.pgpPrivate)) 43 | XCTAssertEqual(scanner.add(segment: privateHeader), .wrongKeyType(.pgpPrivate)) 44 | XCTAssertTrue(scanner.scannedKey.isEmpty) 45 | } 46 | 47 | func testUnknownKeyType() { 48 | XCTAssertEqual(scanner.add(segment: body), .lookingForStart) 49 | XCTAssertEqual(scanner.add(segment: body), .lookingForStart) 50 | XCTAssertTrue(scanner.scannedKey.isEmpty) 51 | } 52 | 53 | func testFooterSplitIntoDifferentSegments() { 54 | XCTAssertEqual(scanner.add(segment: header), .scanned(1)) 55 | XCTAssertEqual(scanner.add(segment: "-----END PGP PUBLIC KEY "), .scanned(2)) 56 | XCTAssertEqual(scanner.add(segment: "BLOCK-----"), .completed) 57 | XCTAssertEqual(scanner.scannedKey, header + footer) 58 | } 59 | 60 | func testFooterWithNewlien() { 61 | XCTAssertEqual(scanner.add(segment: header), .scanned(1)) 62 | XCTAssertEqual(scanner.add(segment: footerWithNewline), .completed) 63 | XCTAssertEqual(scanner.scannedKey, header + footerWithNewline) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /passTests/Models/ScannableKeyTypeTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScannableKeyTypeTest.swift 3 | // passTests 4 | // 5 | // Created by Danny Moesch on 21.08.20. 6 | // Copyright © 2020 Bob Sun. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | @testable import Pass 12 | 13 | final class ScannableKeyTypeTest: XCTestCase { 14 | func testPGPPublicKey() { 15 | let type = ScannableKeyType.pgpPublic 16 | 17 | XCTAssertEqual(type.visibility, "Public") 18 | XCTAssertEqual(type.headerStart, "-----BEGIN PGP PUBLIC KEY BLOCK-----") 19 | XCTAssertEqual(type.footerStart, "-----END PGP PUBLIC") 20 | XCTAssertEqual(type.footerEnd, "KEY BLOCK-----") 21 | XCTAssertEqual(type.counterType, .pgpPrivate) 22 | } 23 | 24 | func testPGPPrivateKey() { 25 | let type = ScannableKeyType.pgpPrivate 26 | 27 | XCTAssertEqual(type.visibility, "Private") 28 | XCTAssertEqual(type.headerStart, "-----BEGIN PGP PRIVATE KEY BLOCK-----") 29 | XCTAssertEqual(type.footerStart, "-----END PGP PRIVATE") 30 | XCTAssertEqual(type.footerEnd, "KEY BLOCK-----") 31 | XCTAssertEqual(type.counterType, .pgpPublic) 32 | } 33 | 34 | func testSSHPrivateKey() { 35 | let type = ScannableKeyType.sshPrivate 36 | 37 | XCTAssertEqual(type.visibility, "Private") 38 | XCTAssertEqual(type.headerStart, "-----BEGIN") 39 | XCTAssertEqual(type.footerStart, "-----END") 40 | XCTAssertEqual(type.footerEnd, "KEY-----") 41 | XCTAssertNil(type.counterType) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /patch/gnu-dummy.patch: -------------------------------------------------------------------------------- 1 | diff --git a/crypto/key.go b/crypto/key.go 2 | index 49af11e..fd59ec4 100644 3 | --- a/crypto/key.go 4 | +++ b/crypto/key.go 5 | @@ -150,10 +150,12 @@ func (key *Key) Unlock(passphrase []byte) (*Key, error) { 6 | return nil, err 7 | } 8 | 9 | - err = unlockedKey.entity.PrivateKey.Decrypt(passphrase) 10 | - if err != nil { 11 | - return nil, errors.Wrap(err, "gopenpgp: error in unlocking key") 12 | - } 13 | + if !unlockedKey.entity.PrivateKey.Dummy() { 14 | + err = unlockedKey.entity.PrivateKey.Decrypt(passphrase) 15 | + if err != nil { 16 | + return nil, errors.Wrap(err, "gopenpgp: error in unlocking key") 17 | + } 18 | + } 19 | 20 | for _, sub := range unlockedKey.entity.Subkeys { 21 | if sub.PrivateKey != nil { 22 | @@ -286,6 +288,10 @@ func (key *Key) IsLocked() (bool, error) { 23 | } 24 | } 25 | 26 | + if key.entity.PrivateKey.Dummy() { 27 | + return true, nil 28 | + } 29 | + 30 | return key.entity.PrivateKey.Encrypted, nil 31 | } 32 | 33 | -------------------------------------------------------------------------------- /scripts/gopenpgp_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euox pipefail 4 | 5 | GOPENPGP_VERSION="v2.8.1-passforios" 6 | 7 | export GOPATH="$(pwd)/go" 8 | export PATH="$PATH:$GOPATH/bin" 9 | 10 | OUTPUT_PATH="go/dist" 11 | CHECKOUT_PATH="go/checkout" 12 | GOPENPGP_PATH="$CHECKOUT_PATH/gopenpgp" 13 | 14 | mkdir -p "$OUTPUT_PATH" 15 | mkdir -p "$CHECKOUT_PATH" 16 | 17 | git clone --depth 1 --branch "$GOPENPGP_VERSION" https://github.com/mssun/gopenpgp.git "$GOPENPGP_PATH" 18 | 19 | pushd "$GOPENPGP_PATH" 20 | mkdir -p dist 21 | go get golang.org/x/mobile/cmd/gomobile@latest 22 | go get golang.org/x/mobile/cmd/gobind@latest 23 | go build golang.org/x/mobile/cmd/gomobile 24 | go build golang.org/x/mobile/cmd/gobind 25 | go mod download github.com/ProtonMail/go-crypto 26 | ./gomobile init 27 | ./gomobile bind -tags mobile -target ios -iosversion 13.0 -v -x -ldflags="-s -w" -o dist/Gopenpgp.xcframework \ 28 | github.com/ProtonMail/gopenpgp/v2/crypto \ 29 | github.com/ProtonMail/gopenpgp/v2/armor \ 30 | github.com/ProtonMail/gopenpgp/v2/constants \ 31 | github.com/ProtonMail/gopenpgp/v2/models \ 32 | github.com/ProtonMail/gopenpgp/v2/subtle \ 33 | github.com/ProtonMail/gopenpgp/v2/helper 34 | popd 35 | 36 | cp -r "$GOPENPGP_PATH/dist/Gopenpgp.xcframework" "$OUTPUT_PATH" 37 | 38 | --------------------------------------------------------------------------------