├── Gemfile ├── Valet tvOS Test Host App ├── Assets.xcassets │ ├── Contents.json │ ├── App Icon & Top Shelf Image.brandassets │ │ ├── App Icon.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Middle.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── App Icon - App Store.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Middle.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Top Shelf Image.imageset │ │ │ └── Contents.json │ │ ├── Top Shelf Image Wide.imageset │ │ │ └── Contents.json │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ └── Contents.json ├── Valet tvOS Test Host App.entitlements ├── AppDelegate.swift ├── ViewController.swift ├── Info.plist └── en.lproj │ └── Main.storyboard ├── Valet watchOS Test Host App Watch App ├── Assets.xcassets │ ├── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── ContentView.swift └── Valet_watchOS_Test_Host_AppApp.swift ├── codecov.yml ├── ValetTouchIDTest ├── ValetTouchIDTest-Bridging-Header.h ├── ValetTouchIDTestAppDelegate.swift ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── ValetTouchIDTestViewController.swift └── Base.lproj │ └── ValetSecureElementTestLaunchScreen.xib ├── Valet.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── Valet watchOS.xcscheme │ ├── ValetTouchIDTest.xcscheme │ ├── Valet iOS Test Host App.xcscheme │ ├── Valet tvOS Test Host App.xcscheme │ ├── Valet macOS Test Host App.xcscheme │ ├── Valet Mac.xcscheme │ ├── Valet iOS.xcscheme │ ├── Valet watchOS Test Host App Watch App.xcscheme │ ├── Valet watchOS Test Host App.xcscheme │ ├── Valet tvOS.xcscheme │ └── Valet watchOS Test Host App Extension.xcscheme ├── Scripts ├── upload-coverage-reports.sh └── build.swift ├── .gitignore ├── Valet watchOS Test Host App ├── Valet watchOS Test Host App.entitlements ├── en.lproj │ └── Interface.storyboard ├── Info.plist └── Assets.xcassets │ └── AppIcon.appiconset │ └── Contents.json ├── BUG-BOUNTY.md ├── Valet watchOS Test Host App Extension ├── Assets.xcassets │ └── Complication.complicationset │ │ ├── Modular.imageset │ │ └── Contents.json │ │ ├── Circular.imageset │ │ └── Contents.json │ │ ├── Extra Large.imageset │ │ └── Contents.json │ │ ├── Utilitarian.imageset │ │ └── Contents.json │ │ └── Contents.json ├── Valet watchOS Test Host App Extension.entitlements ├── ExtensionDelegate.swift ├── InterfaceController.swift └── Info.plist ├── Valet iOS Test Host App ├── Valet iOS Test Host App.entitlements ├── ValetIOSTestHostViewController.swift ├── ValetIOSTestHostAppDelegate.swift ├── Info.plist ├── Base.lproj │ └── LaunchScreen.storyboard ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json └── en.lproj │ └── Main.storyboard ├── Sources ├── Valet │ ├── Valet.h │ ├── Identifier.swift │ ├── Internal │ │ ├── WeakStorage.swift │ │ ├── Configuration.swift │ │ ├── SecItem.swift │ │ └── Service.swift │ ├── CloudAccessibility.swift │ ├── MigrationError.swift │ ├── KeychainError.swift │ ├── SharedGroupIdentifier.swift │ ├── SecureEnclaveAccessControl.swift │ ├── Accessibility.swift │ ├── MigratableKeyValuePair.swift │ └── SecureEnclave.swift └── Info.plist ├── Package.swift ├── Valet macOS Test Host App ├── Valet_macOS_Test_Host_App.entitlements ├── AppDelegate.swift ├── ViewController.swift ├── Info.plist └── Assets.xcassets │ └── AppIcon.appiconset │ └── Contents.json ├── Tests ├── Info.plist ├── ValetIntegrationTests │ ├── SecItemTests.swift │ ├── KeychainIntegrationTests.swift │ └── CloudIntegrationTests.swift ├── ValetTests │ ├── CloudAccessibilityTests.swift │ ├── MigrationErrorTests.swift │ ├── CloudTests.swift │ ├── SinglePromptSecureEnclaveTests.swift │ ├── KeychainErrorTests.swift │ ├── SecureEnclaveTests.swift │ ├── ConfigurationTests.swift │ └── ValetTests.swift └── ValetObjectiveCBridgeTests │ └── VALSecureEnclaveValetTests.m ├── Valet.podspec ├── Gemfile.lock ├── Contributing.md └── .github └── workflows ├── ci.yml └── codeql.yml /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'cocoapods', '~> 1.16.0' 4 | -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Valet watchOS Test Host App Watch App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | comment: 5 | layout: "reach,diff,flags,tree" 6 | behavior: default 7 | require_changes: no 8 | -------------------------------------------------------------------------------- /Valet watchOS Test Host App Watch App/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ValetTouchIDTest/ValetTouchIDTest-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import 6 | -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Valet.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Valet watchOS Test Host App Watch App/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Scripts/upload-coverage-reports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -l 2 | set -ex 3 | 4 | IFS=','; PLATFORMS=$(echo $1); unset IFS 5 | 6 | for PLATFORM in $PLATFORMS; do 7 | bash <(curl -s https://codecov.io/bash) -J '^Valet$' -D .build/derivedData/$PLATFORM -t 5165deef-da9c-443d-90ea-bb0620bffe44 8 | done 9 | -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Valet watchOS Test Host App Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "watchos", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "version" : 1, 9 | "author" : "xcode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "version" : 1, 9 | "author" : "xcode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "version" : 1, 9 | "author" : "xcode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "landscape", 5 | "idiom" : "tv", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "9.0", 8 | "scale" : "1x" 9 | } 10 | ], 11 | "info" : { 12 | "version" : 1, 13 | "author" : "xcode" 14 | } 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | .build/ 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | *.xcworkspace 14 | !default.xcworkspace 15 | *xcuserdata 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | .idea/ 20 | .swiftpm/ 21 | generated/ 22 | *coverage.txt 23 | -------------------------------------------------------------------------------- /Valet watchOS Test Host App/Valet watchOS Test Host App.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.valet.test 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /BUG-BOUNTY.md: -------------------------------------------------------------------------------- 1 | Serious about security 2 | ====================== 3 | 4 | Square recognizes the important contributions the security research community 5 | can make. We therefore encourage reporting security issues with the code 6 | contained in this repository. 7 | 8 | If you believe you have discovered a security vulnerability, please follow the 9 | guidelines at https://bugcrowd.com/squareopensource 10 | -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers" : [ 3 | { 4 | "filename" : "Front.imagestacklayer" 5 | }, 6 | { 7 | "filename" : "Middle.imagestacklayer" 8 | }, 9 | { 10 | "filename" : "Back.imagestacklayer" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers" : [ 3 | { 4 | "filename" : "Front.imagestacklayer" 5 | }, 6 | { 7 | "filename" : "Middle.imagestacklayer" 8 | }, 9 | { 10 | "filename" : "Back.imagestacklayer" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /Valet watchOS Test Host App Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "screenWidth" : "{130,145}", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "screenWidth" : "{146,165}", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /Valet watchOS Test Host App Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "screenWidth" : "{130,145}", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "screenWidth" : "{146,165}", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /Valet watchOS Test Host App Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "screenWidth" : "{130,145}", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "screenWidth" : "{146,165}", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /Valet watchOS Test Host App Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "screenWidth" : "{130,145}", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "screenWidth" : "{146,165}", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /Valet iOS Test Host App/Valet iOS Test Host App.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.valet.test 8 | 9 | keychain-access-groups 10 | 11 | $(AppIdentifierPrefix)com.squareup.Valet-iOS-Test-Host-App 12 | $(AppIdentifierPrefix)com.squareup.Valet-iOS-Test-Host-App2 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Valet tvOS Test Host App.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.valet.test 8 | 9 | keychain-access-groups 10 | 11 | $(AppIdentifierPrefix)com.squareup.Valet-tvOS-Test-Host-App 12 | $(AppIdentifierPrefix)com.squareup.Valet-tvOS-Test-Host-App2 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Valet watchOS Test Host App Extension/Valet watchOS Test Host App Extension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.valet.test 8 | 9 | keychain-access-groups 10 | 11 | $(AppIdentifierPrefix)com.squareup.ValetTouchIDTestApp.watchkitapp.watchkitextension 12 | $(AppIdentifierPrefix)com.squareup.ValetTouchIDTestApp.watchkitapp.watchkitextension2 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Sources/Valet/Valet.h: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/21/15. 2 | // Copyright 2015 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | -------------------------------------------------------------------------------- /Valet watchOS Test Host App Extension/Assets.xcassets/Complication.complicationset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "idiom" : "watch", 5 | "filename" : "Circular.imageset", 6 | "role" : "circular" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "filename" : "Extra Large.imageset", 11 | "role" : "extra-large" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "filename" : "Modular.imageset", 16 | "role" : "modular" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "filename" : "Utilitarian.imageset", 21 | "role" : "utilitarian" 22 | } 23 | ], 24 | "info" : { 25 | "version" : 1, 26 | "author" : "xcode" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Valet", 8 | platforms: [ 9 | .iOS(.v12), 10 | .tvOS(.v12), 11 | .watchOS(.v4), 12 | .macOS(.v10_13), 13 | ], 14 | products: [ 15 | .library( 16 | name: "Valet", 17 | targets: ["Valet"] 18 | ), 19 | ], 20 | targets: [ 21 | .target( 22 | name: "Valet", 23 | dependencies: [], 24 | swiftSettings: [ 25 | .swiftLanguageMode(.v6), 26 | ] 27 | ), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Valet macOS Test Host App/Valet_macOS_Test_Host_App.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | $(TeamIdentifierPrefix)valet.test 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | keychain-access-groups 14 | 15 | $(AppIdentifierPrefix)com.squareup.Valet-macOS-Test-Host-App 16 | $(AppIdentifierPrefix)com.squareup.Valet-macOS-Test-Host-App2 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Tests/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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Valet watchOS Test Host App Extension/ExtensionDelegate.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 3/3/18. 2 | // Copyright © 2018 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import WatchKit 18 | 19 | 20 | class ExtensionDelegate: NSObject, WKExtensionDelegate {} 21 | -------------------------------------------------------------------------------- /Valet iOS Test Host App/ValetIOSTestHostViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/13/17. 2 | // Copyright 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | 20 | class ValetIOSTestHostViewController: UIViewController { 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Valet macOS Test Host App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 9/19/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Cocoa 18 | 19 | 20 | @NSApplicationMain 21 | final class AppDelegate: NSObject, NSApplicationDelegate {} 22 | -------------------------------------------------------------------------------- /Valet tvOS Test Host App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 3/3/18. 2 | // Copyright © 2018 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import UIKit 17 | 18 | 19 | @main 20 | final class AppDelegate: UIResponder, UIApplicationDelegate { 21 | var window: UIWindow? 22 | } 23 | -------------------------------------------------------------------------------- /Valet watchOS Test Host App Extension/InterfaceController.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 3/3/18. 2 | // Copyright © 2018 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import WatchKit 18 | import Foundation 19 | 20 | 21 | class InterfaceController: WKInterfaceController {} 22 | -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "size" : "1280x768", 5 | "idiom" : "tv", 6 | "filename" : "App Icon - App Store.imagestack", 7 | "role" : "primary-app-icon" 8 | }, 9 | { 10 | "size" : "400x240", 11 | "idiom" : "tv", 12 | "filename" : "App Icon.imagestack", 13 | "role" : "primary-app-icon" 14 | }, 15 | { 16 | "size" : "2320x720", 17 | "idiom" : "tv", 18 | "filename" : "Top Shelf Image Wide.imageset", 19 | "role" : "top-shelf-image-wide" 20 | }, 21 | { 22 | "size" : "1920x720", 23 | "idiom" : "tv", 24 | "filename" : "Top Shelf Image.imageset", 25 | "role" : "top-shelf-image" 26 | } 27 | ], 28 | "info" : { 29 | "version" : 1, 30 | "author" : "xcode" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/ValetIntegrationTests/SecItemTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman and Eric Muller on 9/16/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | import XCTest 19 | 20 | @testable import Valet 21 | 22 | 23 | class SecItemTests: XCTestCase { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /ValetTouchIDTest/ValetTouchIDTestAppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Created by Eric Muller on 4/20/16. 2 | // Copyright © 2016 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | 20 | @main 21 | final class ValetTouchIDTestAppDelegate: UIResponder, UIApplicationDelegate { 22 | var window: UIWindow? 23 | } 24 | -------------------------------------------------------------------------------- /Valet iOS Test Host App/ValetIOSTestHostAppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/13/17. 2 | // Copyright 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | 20 | @main 21 | final class ValetIOSTestHostAppDelegate: UIResponder, UIApplicationDelegate { 22 | var window: UIWindow? 23 | } 24 | -------------------------------------------------------------------------------- /Valet watchOS Test Host App Watch App/ContentView.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 4/23/24. 2 | // Copyright © 2024 Block, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | struct ContentView: View { 20 | var body: some View { 21 | VStack { 22 | Text("Hello, world!") 23 | } 24 | .padding() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Valet.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Valet' 3 | s.version = '5.0.0' 4 | s.license = 'Apache License, Version 2.0' 5 | s.summary = 'Securely store data on iOS, tvOS, watchOS, or macOS without knowing a thing about how the Keychain works. It\'s easy. We promise.' 6 | s.homepage = 'https://github.com/square/Valet' 7 | s.authors = 'Square' 8 | s.source = { :git => 'https://github.com/square/Valet.git', :tag => s.version } 9 | s.swift_version = '6.0' 10 | s.source_files = 'Sources/Valet/**/*.{swift,h}' 11 | s.public_header_files = 'Sources/Valet/*.h' 12 | s.frameworks = 'Security' 13 | s.ios.deployment_target = '12.0' 14 | s.tvos.deployment_target = '12.0' 15 | s.watchos.deployment_target = '4.0' 16 | s.macos.deployment_target = '10.13' 17 | 18 | s.tvos.exclude_files = 'Sources/Valet/SinglePromptSecureEnclaveValet.swift' 19 | s.watchos.exclude_files = 'Sources/Valet/SinglePromptSecureEnclaveValet.swift' 20 | end 21 | -------------------------------------------------------------------------------- /Valet watchOS Test Host App Watch App/Valet_watchOS_Test_Host_AppApp.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 4/23/24. 2 | // Copyright © 2024 Block, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | @main 20 | struct Valet_watchOS_Test_Host_App_Watch_AppApp: App { 21 | var body: some Scene { 22 | WindowGroup { 23 | ContentView() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2015 Square, Inc. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Valet watchOS Test Host App/en.lproj/Interface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Valet macOS Test Host App/ViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 9/19/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Cocoa 18 | 19 | class ViewController: NSViewController { 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | // Do any additional setup after loading the view. 25 | } 26 | 27 | override var representedObject: Any? { 28 | didSet { 29 | // Update the view, if already loaded. 30 | } 31 | } 32 | 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /ValetTouchIDTest/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /Valet tvOS Test Host App/ViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 3/3/18. 2 | // Copyright © 2018 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | class ViewController: UIViewController { 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | // Do any additional setup after loading the view, typically from a nib. 24 | } 25 | 26 | override func didReceiveMemoryWarning() { 27 | super.didReceiveMemoryWarning() 28 | // Dispose of any resources that can be recreated. 29 | } 30 | 31 | 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Valet macOS Test Host App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2017 Square, Inc. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /Valet tvOS Test Host App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIMainStoryboardFile 24 | Main 25 | UIRequiredDeviceCapabilities 26 | 27 | arm64 28 | 29 | UIUserInterfaceStyle 30 | Automatic 31 | NSHumanReadableCopyright 32 | Copyright © 2018 Square, Inc. 33 | 34 | 35 | -------------------------------------------------------------------------------- /Valet macOS Test Host App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Valet watchOS Test Host App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | ValetTouchIDTest 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | UISupportedInterfaceOrientations 24 | 25 | UIInterfaceOrientationPortrait 26 | UIInterfaceOrientationPortraitUpsideDown 27 | 28 | WKCompanionAppBundleIdentifier 29 | com.squareup.ValetTouchIDTestApp 30 | WKWatchKitApp 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Valet watchOS Test Host App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "24x24", 5 | "idiom" : "watch", 6 | "scale" : "2x", 7 | "role" : "notificationCenter", 8 | "subtype" : "38mm" 9 | }, 10 | { 11 | "size" : "27.5x27.5", 12 | "idiom" : "watch", 13 | "scale" : "2x", 14 | "role" : "notificationCenter", 15 | "subtype" : "42mm" 16 | }, 17 | { 18 | "size" : "29x29", 19 | "idiom" : "watch", 20 | "role" : "companionSettings", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "size" : "29x29", 25 | "idiom" : "watch", 26 | "role" : "companionSettings", 27 | "scale" : "3x" 28 | }, 29 | { 30 | "size" : "40x40", 31 | "idiom" : "watch", 32 | "scale" : "2x", 33 | "role" : "appLauncher", 34 | "subtype" : "38mm" 35 | }, 36 | { 37 | "size" : "86x86", 38 | "idiom" : "watch", 39 | "scale" : "2x", 40 | "role" : "quickLook", 41 | "subtype" : "38mm" 42 | }, 43 | { 44 | "size" : "98x98", 45 | "idiom" : "watch", 46 | "scale" : "2x", 47 | "role" : "quickLook", 48 | "subtype" : "42mm" 49 | } 50 | ], 51 | "info" : { 52 | "version" : 1, 53 | "author" : "xcode" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Valet/Identifier.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman and Eric Muller on 9/16/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | 20 | public struct Identifier: CustomStringConvertible, Sendable { 21 | 22 | // MARK: Initialization 23 | 24 | public init?(nonEmpty string: String?) { 25 | guard let string = string, !string.isEmpty else { 26 | return nil 27 | } 28 | 29 | backingString = string 30 | } 31 | 32 | // MARK: CustomStringConvertible 33 | 34 | public var description: String { 35 | backingString 36 | } 37 | 38 | // MARK: Private Properties 39 | 40 | private let backingString: String 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Valet/Internal/WeakStorage.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 10/10/24. 2 | // Copyright © 2024 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | 20 | final class WeakStorage: @unchecked Sendable { 21 | subscript(_ key: String) -> T? { 22 | get { 23 | lock.withLock { 24 | identifierToValetMap.object(forKey: key as NSString) 25 | } 26 | } 27 | set { 28 | lock.withLock { 29 | identifierToValetMap.setObject(newValue, forKey: key as NSString) 30 | } 31 | } 32 | } 33 | 34 | private let lock = NSLock() 35 | private let identifierToValetMap = NSMapTable.strongToWeakObjects() 36 | } 37 | -------------------------------------------------------------------------------- /Tests/ValetTests/CloudAccessibilityTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/20/20. 2 | // Copyright © 2020 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | import XCTest 19 | 20 | @testable import Valet 21 | 22 | 23 | final class CloudAccessibilityTests: XCTestCase { 24 | 25 | func test_description_mirrorsAccessibilityCounterpartDescription() { 26 | CloudAccessibility.allCases.forEach { 27 | XCTAssertEqual($0.description, $0.accessibility.description) 28 | } 29 | } 30 | 31 | func test_secAccessibilityAttribute_mirrorsAccessibilityCounterpartSecAccessibilityAttribute() { 32 | CloudAccessibility.allCases.forEach { 33 | XCTAssertEqual($0.secAccessibilityAttribute, $0.accessibility.secAccessibilityAttribute) 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Valet watchOS Test Host App Extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Valet watchOS Test Host App Extension 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 | 1.0 21 | CFBundleVersion 22 | 1 23 | NSExtension 24 | 25 | NSExtensionAttributes 26 | 27 | WKAppBundleIdentifier 28 | com.squareup.ValetTouchIDTestApp.watchkitapp 29 | 30 | NSExtensionPointIdentifier 31 | com.apple.watchkit 32 | 33 | WKExtensionDelegateClassName 34 | $(PRODUCT_MODULE_NAME).ExtensionDelegate 35 | NSHumanReadableCopyright 36 | Copyright © 2018 Square, Inc. 37 | 38 | 39 | -------------------------------------------------------------------------------- /ValetTouchIDTest/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | ValetSecureElementTestLaunchScreen 27 | UIMainStoryboardFile 28 | ValetSecureElementTestMain 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | NSFaceIDUsageDescription 34 | Enables you to use keychain with your face 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Valet iOS Test Host App/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Tests/ValetTests/MigrationErrorTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/20/20. 2 | // Copyright © 2020 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | import XCTest 19 | 20 | @testable import Valet 21 | 22 | 23 | final class MigrationErrorTests: XCTestCase { 24 | 25 | func test_description_createsHumanReadableDescription() { 26 | MigrationError.allCases.forEach { 27 | switch $0 { 28 | case .invalidQuery: 29 | XCTAssertEqual($0.description, "MigrationError.invalidQuery") 30 | case .keyToMigrateInvalid: 31 | XCTAssertEqual($0.description, "MigrationError.keyToMigrateInvalid") 32 | case .dataToMigrateInvalid: 33 | XCTAssertEqual($0.description, "MigrationError.dataToMigrateInvalid") 34 | case .duplicateKeyToMigrate: 35 | XCTAssertEqual($0.description, "MigrationError.duplicateKeyToMigrate") 36 | case .keyToMigrateAlreadyExistsInValet: 37 | XCTAssertEqual($0.description, "MigrationError.keyToMigrateAlreadyExistsInValet") 38 | case .removalFailed: 39 | XCTAssertEqual($0.description, "MigrationError.removalFailed") 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Valet iOS Test Host App/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 | -------------------------------------------------------------------------------- /Tests/ValetTests/CloudTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman and Eric Muller on 9/17/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | import XCTest 19 | 20 | @testable import Valet 21 | 22 | 23 | class CloudTests: XCTestCase 24 | { 25 | static let identifier = Identifier(nonEmpty: "valet_testing")! 26 | static let accessibility = CloudAccessibility.whenUnlocked 27 | let valet = Valet.iCloudValet(with: identifier, accessibility: accessibility) 28 | 29 | // MARK: Equality 30 | 31 | func test_synchronizableValet_isDistinctFromVanillaValetWithEqualConfiguration() 32 | { 33 | let localValet = Valet.valet(with: valet.identifier, accessibility: valet.accessibility) 34 | XCTAssertFalse(valet == localValet) 35 | XCTAssertFalse(valet === localValet) 36 | } 37 | 38 | func test_synchronizableValets_withEquivalentConfigurationsAreEqual() { 39 | guard case let .iCloud(accessibility) = valet.configuration else { 40 | XCTFail() 41 | return 42 | } 43 | let otherValet = Valet.iCloudValet(with: valet.identifier, accessibility: accessibility) 44 | XCTAssertEqual(valet, otherValet, "Valet should be equal to otherValet") 45 | XCTAssertTrue(valet === otherValet, "Valet and otherValet should be the same object") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Valet iOS Test Host App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Valet tvOS Test Host App/en.lproj/Main.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 | 29 | -------------------------------------------------------------------------------- /Valet iOS Test Host App/en.lproj/Main.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 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Sources/Valet/CloudAccessibility.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 9/17/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | 20 | @objc(VALCloudAccessibility) 21 | public enum CloudAccessibility: Int, CaseIterable, CustomStringConvertible, Equatable, Sendable { 22 | /// Valet data can only be accessed while the device is unlocked. This attribute is recommended for data that only needs to be accessible while the application is in the foreground. Valet data with this attribute will migrate to a new device when using encrypted backups. 23 | case whenUnlocked = 1 24 | /// Valet data cannot be accessed after a restart until the device has been unlocked once; data is accessible until the device is next rebooted. This attribute is recommended for data that needs to be accessible by background applications. Valet data with this attribute will migrate to a new device when using encrypted backups. 25 | case afterFirstUnlock = 2 26 | 27 | // MARK: CustomStringConvertible 28 | 29 | public var description: String { 30 | accessibility.description 31 | } 32 | 33 | // MARK: Public Properties 34 | 35 | public var accessibility: Accessibility { 36 | switch self { 37 | case .whenUnlocked: 38 | return .whenUnlocked 39 | case .afterFirstUnlock: 40 | return .afterFirstUnlock 41 | } 42 | } 43 | 44 | public var secAccessibilityAttribute: String { 45 | accessibility.secAccessibilityAttribute 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Tests/ValetIntegrationTests/KeychainIntegrationTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 5/20/20. 2 | // Copyright © 2020 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | import XCTest 19 | 20 | @testable import Valet 21 | 22 | 23 | final class KeychainIntegrationTests: XCTestCase { 24 | 25 | func test_revertMigration_removesAllMigratedKeys() throws { 26 | guard testEnvironmentIsSignedOrDoesNotRequireEntitlement() else { 27 | return 28 | } 29 | 30 | let migrationValet = Valet.valet(with: Identifier(nonEmpty: "Migrate_Me")!, accessibility: .afterFirstUnlock) 31 | try migrationValet.removeAllObjects() 32 | 33 | let anotherValet = Valet.valet(with: Identifier(nonEmpty: #function)!, accessibility: .whenUnlocked) 34 | try anotherValet.removeAllObjects() 35 | 36 | let keyValuePairsToMigrate = [ 37 | "yo": "dawg", 38 | "we": "heard", 39 | "you": "like", 40 | "migrating": "to", 41 | "other": "valets" 42 | ] 43 | 44 | for (key, value) in keyValuePairsToMigrate { 45 | try migrationValet.setString(value, forKey: key) 46 | } 47 | 48 | try anotherValet.setString("password", forKey: "accountName") 49 | try anotherValet.migrateObjects(from: migrationValet, removeOnCompletion: false) 50 | Keychain.revertMigration(into: anotherValet.baseKeychainQuery, keysInKeychainPreMigration: Set(["accountName"])) 51 | 52 | XCTAssertEqual(try anotherValet.allKeys().count, 1) 53 | XCTAssertEqual(try anotherValet.string(forKey: "accountName"), "password") 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Valet/MigrationError.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman and Eric Muller on 9/16/17. 2 | // Copyright © 2017 Square Inc. 3 | // 4 | // Licensed under the Apache License Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing software 11 | // distributed under the License is distributed on an "AS IS" BASIS 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | 20 | @objc(VALMigrationResult) 21 | public enum MigrationError: Int, CaseIterable, CustomStringConvertible, Error, Equatable, Sendable { 22 | /// Migration failed because the keychain query was not valid. 23 | case invalidQuery 24 | /// Migration failed because a key staged for migration was invalid. 25 | case keyToMigrateInvalid 26 | /// Migration failed because some data staged for migration was invalid. 27 | case dataToMigrateInvalid 28 | /// Migration failed because two equivalent keys were staged for migration. 29 | case duplicateKeyToMigrate 30 | /// Migration failed because a key staged for migration duplicates a key already managed by Valet. 31 | case keyToMigrateAlreadyExistsInValet 32 | /// Migration failed because removing the migrated data from the keychain failed. 33 | case removalFailed 34 | 35 | // MARK: CustomStringConvertible 36 | 37 | public var description: String { 38 | switch self { 39 | case .invalidQuery: return "MigrationError.invalidQuery" 40 | case .keyToMigrateInvalid: return "MigrationError.keyToMigrateInvalid" 41 | case .dataToMigrateInvalid: return "MigrationError.dataToMigrateInvalid" 42 | case .duplicateKeyToMigrate: return "MigrationError.duplicateKeyToMigrate" 43 | case .keyToMigrateAlreadyExistsInValet: return "MigrationError.keyToMigrateAlreadyExistsInValet" 44 | case .removalFailed: return "MigrationError.removalFailed" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Valet/Internal/Configuration.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman and Eric Muller on 9/16/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | 20 | enum Configuration: CustomStringConvertible, Sendable { 21 | case valet(Accessibility) 22 | case iCloud(CloudAccessibility) 23 | case secureEnclave(SecureEnclaveAccessControl) 24 | case singlePromptSecureEnclave(SecureEnclaveAccessControl) 25 | 26 | // MARK: CustomStringConvertible 27 | 28 | var description: String { 29 | switch self { 30 | case .valet: 31 | return "VALValet" 32 | case .iCloud: 33 | return "VALSynchronizableValet" 34 | case .secureEnclave: 35 | return "VALSecureEnclaveValet" 36 | case .singlePromptSecureEnclave: 37 | return "VALSinglePromptSecureEnclaveValet" 38 | } 39 | } 40 | 41 | // MARK: Internal Properties 42 | 43 | var accessibility: Accessibility { 44 | switch self { 45 | case let .valet(accessibility): 46 | return accessibility 47 | case let .iCloud(cloudAccessibility): 48 | return cloudAccessibility.accessibility 49 | case .secureEnclave, .singlePromptSecureEnclave: 50 | return Accessibility.whenPasscodeSetThisDeviceOnly 51 | } 52 | } 53 | 54 | var prettyDescription: String { 55 | let configurationDescription: String = { 56 | switch self { 57 | case .valet: 58 | return "(Valet)" 59 | case .iCloud: 60 | return "(iCloud)" 61 | case .secureEnclave: 62 | return "(Secure Enclave)" 63 | case .singlePromptSecureEnclave: 64 | return "(Single Prompt)" 65 | } 66 | }() 67 | return "\(accessibility) \(configurationDescription)" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/ValetTests/SinglePromptSecureEnclaveTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Eric Muller on 10/1/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) 18 | 19 | import Foundation 20 | @testable import Valet 21 | import XCTest 22 | 23 | 24 | class SinglePromptSecureEnclaveTests: XCTestCase 25 | { 26 | static let identifier = Identifier(nonEmpty: "valet_testing")! 27 | let valet = SinglePromptSecureEnclaveValet.valet(with: SinglePromptSecureEnclaveTests.identifier, accessControl: .userPresence) 28 | 29 | // MARK: Initialization 30 | 31 | func test_init_createsCorrectBackingService() { 32 | let identifier = ValetTests.identifier 33 | 34 | SecureEnclaveAccessControl.allValues().forEach { accessControl in 35 | let backingService = SinglePromptSecureEnclaveValet.valet(with: identifier, accessControl: accessControl).service 36 | XCTAssertEqual(backingService, Service.standard(identifier, .singlePromptSecureEnclave(accessControl))) 37 | } 38 | } 39 | 40 | func test_init_createsCorrectBackingService_sharedAccess() { 41 | let identifier = Valet.sharedAccessGroupIdentifier 42 | 43 | SecureEnclaveAccessControl.allValues().forEach { accessControl in 44 | let backingService = SinglePromptSecureEnclaveValet.sharedGroupValet(with: identifier, accessControl: accessControl).service 45 | XCTAssertEqual(backingService, Service.sharedGroup(identifier, nil, .singlePromptSecureEnclave(accessControl))) 46 | } 47 | } 48 | 49 | // MARK: Equality 50 | 51 | func test_SinglePromptSecureEnclaveValetsWithEqualConfiguration_haveEqualPointers() 52 | { 53 | let equivalentValet = SinglePromptSecureEnclaveValet.valet(with: valet.identifier, accessControl: valet.accessControl) 54 | XCTAssertTrue(valet == equivalentValet) 55 | XCTAssertTrue(valet === equivalentValet) 56 | } 57 | } 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /Sources/Valet/KeychainError.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman and Eric Muller on 9/16/17. 2 | // Copyright © 2017 Square Inc. 3 | // 4 | // Licensed under the Apache License Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing software 11 | // distributed under the License is distributed on an "AS IS" BASIS 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | 20 | @objc(VALKeychainError) 21 | public enum KeychainError: Int, CaseIterable, CustomStringConvertible, Error, Equatable, Sendable { 22 | /// The keychain could not be accessed. 23 | case couldNotAccessKeychain 24 | /// User dismissed the user-presence prompt. 25 | case userCancelled 26 | /// No data was found for the requested key. 27 | case itemNotFound 28 | /// The application does not have the proper entitlements to perform the requested action. 29 | /// This may be due to an Apple Keychain bug. As a workaround try running on a device that is not attached to a debugger. 30 | /// - SeeAlso: https://forums.developer.apple.com/forums/thread/4743 31 | case missingEntitlement 32 | /// The key provided is empty. 33 | case emptyKey 34 | /// The value provided is empty. 35 | case emptyValue 36 | 37 | init(status: OSStatus) { 38 | switch status { 39 | case errSecItemNotFound: 40 | self = .itemNotFound 41 | case errSecUserCanceled, 42 | errSecAuthFailed: 43 | self = .userCancelled 44 | case errSecMissingEntitlement: 45 | self = .missingEntitlement 46 | default: 47 | self = .couldNotAccessKeychain 48 | } 49 | } 50 | 51 | // MARK: CustomStringConvertible 52 | 53 | public var description: String { 54 | switch self { 55 | case .couldNotAccessKeychain: return "KeychainError.couldNotAccessKeychain" 56 | case .emptyKey: return "KeychainError.emptyKey" 57 | case .emptyValue: return "KeychainError.emptyValue" 58 | case .itemNotFound: return "KeychainError.itemNotFound" 59 | case .missingEntitlement: return "KeychainError.missingEntitlement" 60 | case .userCancelled: return "KeychainError.userCancelled" 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Tests/ValetTests/KeychainErrorTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/20/20. 2 | // Copyright © 2020 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | import XCTest 19 | 20 | @testable import Valet 21 | 22 | 23 | final class KeychainErrorTests: XCTestCase { 24 | 25 | func test_initStatus_createsNotFoundErrorFrom_errSecItemNotFound() { 26 | XCTAssertEqual(KeychainError(status: errSecItemNotFound), KeychainError.itemNotFound) 27 | } 28 | 29 | func test_initStatus_createsUserCancelledFrom_errSecUserCanceled() { 30 | XCTAssertEqual(KeychainError(status: errSecUserCanceled), KeychainError.userCancelled) 31 | } 32 | 33 | func test_initStatus_createsUserCancelledFrom_errSecAuthFailed() { 34 | XCTAssertEqual(KeychainError(status: errSecAuthFailed), KeychainError.userCancelled) 35 | } 36 | 37 | func test_initStatus_createsMissingEntitlementFrom_errSecMissingEntitlement() { 38 | XCTAssertEqual(KeychainError(status: errSecMissingEntitlement), KeychainError.missingEntitlement) 39 | } 40 | 41 | func test_initStatus_createsCouldNotAccessKeychainFrom_errSecNotAvailable() { 42 | XCTAssertEqual(KeychainError(status: errSecNotAvailable), KeychainError.couldNotAccessKeychain) 43 | } 44 | 45 | func test_description_createsHumanReadableDescription() { 46 | KeychainError.allCases.forEach { 47 | switch $0 { 48 | case .couldNotAccessKeychain: 49 | XCTAssertEqual($0.description, "KeychainError.couldNotAccessKeychain") 50 | case .emptyKey: 51 | XCTAssertEqual($0.description, "KeychainError.emptyKey") 52 | case .emptyValue: 53 | XCTAssertEqual($0.description, "KeychainError.emptyValue") 54 | case .itemNotFound: 55 | XCTAssertEqual($0.description, "KeychainError.itemNotFound") 56 | case .missingEntitlement: 57 | XCTAssertEqual($0.description, "KeychainError.missingEntitlement") 58 | case .userCancelled: 59 | XCTAssertEqual($0.description, "KeychainError.userCancelled") 60 | } 61 | 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/ValetTests/SecureEnclaveTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman and Eric Muller on 9/17/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | import XCTest 19 | 20 | @testable import Valet 21 | 22 | 23 | class SecureEnclaveTests: XCTestCase 24 | { 25 | static let identifier = Identifier(nonEmpty: "valet_testing")! 26 | let valet = SecureEnclaveValet.valet(with: identifier, accessControl: .userPresence) 27 | 28 | // MARK: Initialization 29 | 30 | func test_init_createsCorrectBackingService() { 31 | let identifier = ValetTests.identifier 32 | 33 | SecureEnclaveAccessControl.allValues().forEach { accessControl in 34 | let backingService = SecureEnclaveValet.valet(with: identifier, accessControl: accessControl).service 35 | XCTAssertEqual(backingService, Service.standard(identifier, .secureEnclave(accessControl))) 36 | } 37 | } 38 | 39 | func test_init_createsCorrectBackingService_sharedAccess() { 40 | let identifier = Valet.sharedAccessGroupIdentifier 41 | 42 | SecureEnclaveAccessControl.allValues().forEach { accessControl in 43 | let backingService = SecureEnclaveValet.sharedGroupValet(with: identifier, accessControl: accessControl).service 44 | XCTAssertEqual(backingService, Service.sharedGroup(identifier, nil, .secureEnclave(accessControl))) 45 | } 46 | } 47 | 48 | func test_init_createsCorrectBackingService_sharedAccess_withIdentifier() { 49 | let groupIdentifier = Valet.sharedAccessGroupIdentifier 50 | let identifier = Identifier(nonEmpty: "id") 51 | 52 | SecureEnclaveAccessControl.allValues().forEach { accessControl in 53 | let backingService = SecureEnclaveValet.sharedGroupValet(with: groupIdentifier, identifier: identifier, accessControl: accessControl).service 54 | XCTAssertEqual(backingService, Service.sharedGroup(groupIdentifier, identifier, .secureEnclave(accessControl))) 55 | } 56 | } 57 | 58 | // MARK: Equality 59 | 60 | func test_secureEnclaveValetsWithEqualConfiguration_haveEqualPointers() 61 | { 62 | let equivalentValet = SecureEnclaveValet.valet(with: valet.identifier, accessControl: valet.accessControl) 63 | XCTAssertTrue(valet == equivalentValet) 64 | XCTAssertTrue(valet === equivalentValet) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (7.2.1.2) 9 | base64 10 | bigdecimal 11 | concurrent-ruby (~> 1.0, >= 1.3.1) 12 | connection_pool (>= 2.2.5) 13 | drb 14 | i18n (>= 1.6, < 2) 15 | logger (>= 1.4.2) 16 | minitest (>= 5.1) 17 | securerandom (>= 0.3) 18 | tzinfo (~> 2.0, >= 2.0.5) 19 | addressable (2.8.7) 20 | public_suffix (>= 2.0.2, < 7.0) 21 | algoliasearch (1.27.5) 22 | httpclient (~> 2.8, >= 2.8.3) 23 | json (>= 1.5.1) 24 | atomos (0.1.3) 25 | base64 (0.2.0) 26 | bigdecimal (3.1.8) 27 | claide (1.1.0) 28 | cocoapods (1.16.1) 29 | addressable (~> 2.8) 30 | claide (>= 1.0.2, < 2.0) 31 | cocoapods-core (= 1.16.1) 32 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 33 | cocoapods-downloader (>= 2.1, < 3.0) 34 | cocoapods-plugins (>= 1.0.0, < 2.0) 35 | cocoapods-search (>= 1.0.0, < 2.0) 36 | cocoapods-trunk (>= 1.6.0, < 2.0) 37 | cocoapods-try (>= 1.1.0, < 2.0) 38 | colored2 (~> 3.1) 39 | escape (~> 0.0.4) 40 | fourflusher (>= 2.3.0, < 3.0) 41 | gh_inspector (~> 1.0) 42 | molinillo (~> 0.8.0) 43 | nap (~> 1.0) 44 | ruby-macho (>= 2.3.0, < 3.0) 45 | xcodeproj (>= 1.26.0, < 2.0) 46 | cocoapods-core (1.16.1) 47 | activesupport (>= 5.0, < 8) 48 | addressable (~> 2.8) 49 | algoliasearch (~> 1.0) 50 | concurrent-ruby (~> 1.1) 51 | fuzzy_match (~> 2.0.4) 52 | nap (~> 1.0) 53 | netrc (~> 0.11) 54 | public_suffix (~> 4.0) 55 | typhoeus (~> 1.0) 56 | cocoapods-deintegrate (1.0.5) 57 | cocoapods-downloader (2.1) 58 | cocoapods-plugins (1.0.0) 59 | nap 60 | cocoapods-search (1.0.1) 61 | cocoapods-trunk (1.6.0) 62 | nap (>= 0.8, < 2.0) 63 | netrc (~> 0.11) 64 | cocoapods-try (1.2.0) 65 | colored2 (3.1.2) 66 | concurrent-ruby (1.3.4) 67 | connection_pool (2.4.1) 68 | drb (2.2.1) 69 | escape (0.0.4) 70 | ethon (0.16.0) 71 | ffi (>= 1.15.0) 72 | ffi (1.17.0) 73 | fourflusher (2.3.1) 74 | fuzzy_match (2.0.4) 75 | gh_inspector (1.1.3) 76 | httpclient (2.8.3) 77 | i18n (1.14.6) 78 | concurrent-ruby (~> 1.0) 79 | json (2.7.5) 80 | logger (1.6.1) 81 | minitest (5.25.1) 82 | molinillo (0.8.0) 83 | nanaimo (0.4.0) 84 | nap (1.1.0) 85 | netrc (0.11.0) 86 | nkf (0.2.0) 87 | public_suffix (4.0.7) 88 | rexml (3.4.2) 89 | ruby-macho (2.5.1) 90 | securerandom (0.3.1) 91 | typhoeus (1.4.1) 92 | ethon (>= 0.9.0) 93 | tzinfo (2.0.6) 94 | concurrent-ruby (~> 1.0) 95 | xcodeproj (1.26.0) 96 | CFPropertyList (>= 2.3.3, < 4.0) 97 | atomos (~> 0.1.3) 98 | claide (>= 1.0.2, < 2.0) 99 | colored2 (~> 3.1) 100 | nanaimo (~> 0.4.0) 101 | rexml (>= 3.3.6, < 4.0) 102 | 103 | PLATFORMS 104 | ruby 105 | 106 | DEPENDENCIES 107 | cocoapods (~> 1.16.0) 108 | 109 | BUNDLED WITH 110 | 2.5.16 111 | -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | ### Sign the CLA 2 | 3 | All contributors to your PR must sign our [Individual Contributor License Agreement (CLA)](https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1). The CLA is a short form that ensures that you are eligible to contribute. 4 | 5 | ### One issue or bug per Pull Request 6 | 7 | Keep your Pull Requests small. Small PRs are easier to reason about which makes them significantly more likely to get merged. 8 | 9 | ### Issues before features 10 | 11 | If you want to add a feature, please file an [Issue](https://github.com/square/Valet/issues) first. An Issue gives us the opportunity to discuss the requirements and implications of a feature with you before you start writing code. 12 | 13 | ### Backwards compatibility 14 | 15 | Respect the minimum deployment target. If you are adding code that uses new APIs, make sure to prevent older clients from crashing or misbehaving. 16 | 17 | ### Forwards compatibility 18 | 19 | Please do not write new code using deprecated APIs. 20 | 21 | ### Testing changes on macOS 22 | 23 | When making changes that change how the keychain works on macOS, you must test this change locally. Unfortunately, running our integration test suite on macOS requires a signed environment, and the esoteric nature of codesigning on macOS means we currently cannot run these tests in CI. 24 | 25 | To run macOS tests locally, you'll need to do the following in the Valet Xcode project settings: 26 | 27 | 1. Read through all the following steps before starting this process! 28 | 1. Commit your current work to `git` so you can easily clear the following changes. 29 | 1. Select the `Valet Mac Tests` target's "General" settings and change the "Host Application" setting from `None` to `Valet macOS Test Host App` 30 | 1. Select the `Valet Mac Tests` target's "Signing & Capabilities" settings and change the "Team" to be your personal team. 31 | 1. Select the `Valet macOS Test Host App` target's "Signing & Capabilities" settings and select the "Team" to be your personal team. This will result in an error – this is expected and continuing to follow the below steps will resolve the error. 32 | 1. Select the `Valet macOS Test Host App` target's "Signing & Capabilities" settings and change the Bundle Identifier to be a unique bundle identifier that references your team name. Run a find and replace over the code to change all `com.squareup.Valet-macOS-Test-Host-App` strings to be your new bundle identifier. 33 | 1. Run a find and replace over the code to change all instances of `9XUJ7M53NG` to reference your personal team ID. Your personal team ID is the same as the prefix shown in the App Groups entitlement. 34 | 1. Make the `_sharedAccessGroupPrefix` method of `VALLegacyValet` return your personal team ID by adding `return @"Your_Team_ID_Here";` to the first line of this method. 35 | 1. Select the `Valet Mac` scheme. 36 | 37 | You can now run all macOS tests locally. Note that you will be required to enter your computer password _many_ times in order for the tests to successfully complete. Failing to enter your password will cause a test to fail. Make sure not to commit these project configuration and code changes after testing your change. 38 | 39 | If you encounter entitlement errors when running tests after following the above steps, you can address by opening `/System/Applications/Utilities/Keychain\ Access.app` and deleting all entries that start with `VAL_VAL`. Note that this will delete any secrets from your macOS applications that utilize Valet, so this step should be taken only as a last resort. 40 | -------------------------------------------------------------------------------- /Valet.xcodeproj/xcshareddata/xcschemes/Valet watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 62 | 63 | 69 | 70 | 76 | 77 | 78 | 79 | 81 | 82 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /Valet.xcodeproj/xcshareddata/xcschemes/ValetTouchIDTest.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | schedule: 8 | # Runs at 00:00 UTC every Monday to ensure that CI does not bitrot. 9 | - cron: '0 0 * * 1' 10 | pull_request: 11 | 12 | concurrency: 13 | group: ${{ github.ref_name }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | xcode-build-16: 18 | name: Xcode 16 Build 19 | runs-on: macOS-15 20 | strategy: 21 | matrix: 22 | platforms: [ 23 | 'iOS_18', 24 | 'tvOS_18', 25 | 'watchOS_11', 26 | ] 27 | fail-fast: false 28 | timeout-minutes: 30 29 | permissions: 30 | contents: read 31 | steps: 32 | - name: Checkout Repo 33 | uses: actions/checkout@v5 34 | - name: Bundle Install 35 | run: bundle install 36 | - name: Select Xcode Version 37 | run: sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer 38 | - name: Build and Test Framework 39 | run: Scripts/build.swift ${{ matrix.platforms }} xcode 40 | - name: Upload Coverage Reports 41 | if: success() 42 | run: Scripts/upload-coverage-reports.sh ${{ matrix.platforms }} 43 | pod-lint: 44 | name: Pod Lint 45 | runs-on: macOS-15 46 | timeout-minutes: 30 47 | permissions: 48 | contents: read 49 | steps: 50 | - name: Checkout Repo 51 | uses: actions/checkout@v5 52 | - name: Bundle Install 53 | run: bundle install 54 | - name: Select Xcode Version 55 | run: sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer 56 | - name: Lint Podspec 57 | run: bundle exec pod lib lint --verbose --fail-fast --swift-version=6.0 --allow-warnings # Cocoapods v1.6 now warns about potential naming colisions. We can fix this in the next breaking change. 58 | carthage: 59 | name: Carthage 60 | runs-on: macOS-15 61 | timeout-minutes: 30 62 | permissions: 63 | contents: read 64 | steps: 65 | - name: Checkout Repo 66 | uses: actions/checkout@v5 67 | - name: Bundle Install 68 | run: bundle install 69 | - name: Select Xcode Version 70 | run: sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer 71 | - name: Install Carthage 72 | run: brew outdated carthage || brew upgrade carthage 73 | - name: Build Framework 74 | run: carthage build --verbose --no-skip-current --use-xcframeworks 75 | spm-16: 76 | name: SPM Build macOS 15 77 | runs-on: macOS-15 78 | strategy: 79 | matrix: 80 | platforms: [ 81 | 'iOS_18', 82 | 'tvOS_18', 83 | 'watchOS_11', 84 | 'macOS_15', 85 | ] 86 | fail-fast: false 87 | timeout-minutes: 30 88 | permissions: 89 | contents: read 90 | steps: 91 | - name: Checkout Repo 92 | uses: actions/checkout@v5 93 | - name: Bundle Install 94 | run: bundle install 95 | - name: Select Xcode Version 96 | run: sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer 97 | - name: Build Framework 98 | run: Scripts/build.swift ${{ matrix.platforms }} spm 99 | readme-validation: 100 | name: Check Markdown links 101 | runs-on: ubuntu-latest 102 | permissions: 103 | contents: read 104 | steps: 105 | - name: Checkout Repo 106 | uses: actions/checkout@v5 107 | - name: Link Checker 108 | uses: AlexanderDokuchaev/md-dead-link-check@d5a37e0b14e5918605d22b34562532762ccb2e47 # v1.2.0 109 | -------------------------------------------------------------------------------- /Valet.xcodeproj/xcshareddata/xcschemes/Valet iOS Test Host App.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Valet.xcodeproj/xcshareddata/xcschemes/Valet tvOS Test Host App.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Valet.xcodeproj/xcshareddata/xcschemes/Valet macOS Test Host App.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Sources/Valet/SharedGroupIdentifier.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 2/25/20. 2 | // Copyright © 2020 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | 20 | public struct SharedGroupIdentifier: CustomStringConvertible, Sendable { 21 | 22 | // MARK: Initialization 23 | 24 | /// A representation of a shared access group identifier. 25 | /// - Parameters: 26 | /// - appIDPrefix: The application's App ID prefix. This string can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty. 27 | /// - groupIdentifier: An identifier that cooresponds to a value in keychain-access-groups in the application's Entitlements file. This string must not be empty. 28 | /// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps 29 | public init?(appIDPrefix: String, nonEmptyGroup groupIdentifier: String?) { 30 | guard !appIDPrefix.isEmpty, let groupIdentifier = groupIdentifier, !groupIdentifier.isEmpty else { 31 | return nil 32 | } 33 | 34 | self.prefix = appIDPrefix 35 | self.groupIdentifier = groupIdentifier 36 | } 37 | 38 | /// A representation of a shared app group identifier. 39 | /// - Parameters: 40 | /// - groupPrefix: On iOS, iPadOS, watchOS, and tvOS, this prefix must equal "group". On macOS, this prefix is the application's App ID prefix, which can be found by inspecting the application's provisioning profile, or viewing the application's App ID Configuration on developer.apple.com. This string must not be empty. 41 | /// - groupIdentifier: An identifier that corresponds to a value in com.apple.security.application-groups in the application's Entitlements file. This string must not be empty. 42 | /// - SeeAlso: https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps 43 | public init?(groupPrefix: String, nonEmptyGroup groupIdentifier: String?) { 44 | #if os(macOS) 45 | guard !groupPrefix.isEmpty, let groupIdentifier = groupIdentifier, !groupIdentifier.isEmpty else { 46 | return nil 47 | } 48 | #else 49 | guard groupPrefix == Self.appGroupPrefix, let groupIdentifier = groupIdentifier, !groupIdentifier.isEmpty else { 50 | return nil 51 | } 52 | #endif 53 | 54 | self.prefix = groupPrefix 55 | self.groupIdentifier = groupIdentifier 56 | } 57 | 58 | // MARK: CustomStringConvertible 59 | 60 | public var description: String { 61 | prefix + "." + groupIdentifier 62 | } 63 | 64 | // MARK: Internal Properties 65 | 66 | let prefix: String 67 | let groupIdentifier: String 68 | 69 | var asIdentifier: Identifier { 70 | // It is safe to force unwrap because we've already validated that our description is non-empty. 71 | Identifier(nonEmpty: description)! 72 | } 73 | 74 | // MARK: Private Static Properties 75 | 76 | private static let appGroupPrefix = "group" 77 | } 78 | -------------------------------------------------------------------------------- /Valet.xcodeproj/xcshareddata/xcschemes/Valet Mac.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /Valet.xcodeproj/xcshareddata/xcschemes/Valet iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /Sources/Valet/SecureEnclaveAccessControl.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 9/18/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | 20 | @objc(VALSecureEnclaveAccessControl) 21 | public enum SecureEnclaveAccessControl: Int, CustomStringConvertible, Equatable, Sendable { 22 | /// Access to keychain elements requires user presence verification via Touch ID, Face ID, or device Passcode. On macOS 10.15 and later, this element may also be accessed via a prompt on a paired watch. Keychain elements are still accessible by Touch ID even if fingers are added or removed. Touch ID does not have to be available or enrolled. 23 | case userPresence = 1 24 | 25 | /// Access to keychain elements requires user presence verification via Face ID, or any finger enrolled in Touch ID. Keychain elements remain accessible via Face ID or Touch ID after faces or fingers are added or removed. Face ID must be enabled with at least one face enrolled, or Touch ID must be available and at least one finger must be enrolled. 26 | case biometricAny 27 | 28 | /// Access to keychain elements requires user presence verification via the face currently enrolled in Face ID, or fingers currently enrolled in Touch ID. Previously written keychain elements become inaccessible when faces or fingers are added or removed. Face ID must be enabled with at least one face enrolled, or Touch ID must be available and at least one finger must be enrolled. 29 | case biometricCurrentSet 30 | 31 | /// Access to keychain elements requires user presence verification via device Passcode. 32 | case devicePasscode 33 | 34 | // MARK: CustomStringConvertible 35 | 36 | public var description: String { 37 | switch self { 38 | case .userPresence: 39 | /* 40 | VALSecureEnclaveValet v1.0-v2.0.7 used UserPresence without a suffix – the concept of a customizable AccessControl was added in v2.1. 41 | For backwards compatibility, do not append an access control suffix for UserPresence. 42 | */ 43 | return "" 44 | case .biometricAny: 45 | return "_AccessControlTouchIDAnyFingerprint" 46 | case .biometricCurrentSet: 47 | return "_AccessControlTouchIDCurrentFingerprintSet" 48 | case .devicePasscode: 49 | return "_AccessControlDevicePasscode" 50 | } 51 | } 52 | 53 | // MARK: Internal Properties 54 | 55 | var secAccessControl: SecAccessControlCreateFlags { 56 | switch self { 57 | case .userPresence: 58 | .userPresence 59 | case .biometricAny: 60 | if #available(watchOS 4.3, macOS 10.13.4, *) { 61 | .biometryAny 62 | } else { 63 | .touchIDAny 64 | } 65 | case .biometricCurrentSet: 66 | if #available(watchOS 4.3, macOS 10.13.4, *) { 67 | .biometryCurrentSet 68 | } else { 69 | .touchIDCurrentSet 70 | } 71 | case .devicePasscode: 72 | .devicePasscode 73 | } 74 | } 75 | 76 | static func allValues() -> [SecureEnclaveAccessControl] { 77 | [ 78 | .userPresence, 79 | .devicePasscode, 80 | .biometricAny, 81 | .biometricCurrentSet, 82 | ] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/ValetTests/ConfigurationTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/20/20. 2 | // Copyright © 2020 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | import XCTest 19 | 20 | @testable import Valet 21 | 22 | 23 | final class ConfigurationTests: XCTestCase { 24 | 25 | func test_description_valet_mirrorsLegacyName() { 26 | Accessibility.allCases.forEach { 27 | XCTAssertEqual(Configuration.valet($0).description, "VALValet") 28 | } 29 | } 30 | 31 | func test_description_iCloud_mirrorsLegacyName() { 32 | CloudAccessibility.allCases.forEach { 33 | XCTAssertEqual(Configuration.iCloud($0).description, "VALSynchronizableValet") 34 | } 35 | } 36 | 37 | func test_description_secureEnclave_mirrorsLegacyName() { 38 | SecureEnclaveAccessControl.allValues().forEach { 39 | XCTAssertEqual(Configuration.secureEnclave($0).description, "VALSecureEnclaveValet") 40 | } 41 | } 42 | 43 | func test_description_singlePromptSecureEnclave_mirrorsLegacyName() { 44 | SecureEnclaveAccessControl.allValues().forEach { 45 | XCTAssertEqual(Configuration.singlePromptSecureEnclave($0).description, "VALSinglePromptSecureEnclaveValet") 46 | } 47 | } 48 | 49 | func test_accessibility_valet_returnsPassedInAccessibility() { 50 | Accessibility.allCases.forEach { 51 | XCTAssertEqual(Configuration.valet($0).accessibility, $0) 52 | } 53 | } 54 | 55 | func test_accessibility_iCloud_returnsPassedInAccessibility() { 56 | CloudAccessibility.allCases.forEach { 57 | XCTAssertEqual(Configuration.iCloud($0).accessibility, $0.accessibility) 58 | } 59 | } 60 | 61 | func test_accessibility_secureEnclave_returnsWhenPassCodeSetThisDeviceOnly() { 62 | SecureEnclaveAccessControl.allValues().forEach { 63 | XCTAssertEqual(Configuration.secureEnclave($0).accessibility, Accessibility.whenPasscodeSetThisDeviceOnly) 64 | } 65 | } 66 | 67 | func test_accessibility_singlePromptSecureEnclave_returnsWhenPassCodeSetThisDeviceOnly() { 68 | SecureEnclaveAccessControl.allValues().forEach { 69 | XCTAssertEqual(Configuration.singlePromptSecureEnclave($0).accessibility, Accessibility.whenPasscodeSetThisDeviceOnly) 70 | } 71 | } 72 | 73 | func test_prettyDescription_valet_isHumanReadable() { 74 | Accessibility.allCases.forEach { 75 | XCTAssertEqual(Configuration.valet($0).prettyDescription, "\($0) (Valet)") 76 | } 77 | } 78 | 79 | func test_prettyDescription_iCloud_isHumanReadable() { 80 | CloudAccessibility.allCases.forEach { 81 | XCTAssertEqual(Configuration.iCloud($0).prettyDescription, "\($0) (iCloud)") 82 | } 83 | } 84 | 85 | func test_prettyDescription_secureEnclave_isHumanReadable() { 86 | SecureEnclaveAccessControl.allValues().forEach { 87 | XCTAssertEqual(Configuration.secureEnclave($0).prettyDescription, "\(Accessibility.whenPasscodeSetThisDeviceOnly) (Secure Enclave)") 88 | } 89 | } 90 | 91 | func test_prettyDescription_singlePromptSecureEnclave_isHumanReadable() { 92 | SecureEnclaveAccessControl.allValues().forEach { 93 | XCTAssertEqual(Configuration.singlePromptSecureEnclave($0).prettyDescription, "\(Accessibility.whenPasscodeSetThisDeviceOnly) (Single Prompt)") 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /Valet.xcodeproj/xcshareddata/xcschemes/Valet watchOS Test Host App Watch App.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 46 | 47 | 57 | 59 | 65 | 66 | 67 | 68 | 74 | 76 | 82 | 83 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Tests/ValetIntegrationTests/CloudIntegrationTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman and Eric Muller on 9/17/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | import XCTest 19 | 20 | @testable import Valet 21 | 22 | 23 | class CloudIntegrationTests: XCTestCase 24 | { 25 | static let identifier = Valet.sharedAccessGroupIdentifier 26 | static let accessibility = CloudAccessibility.whenUnlocked 27 | var allPermutations: [Valet] { 28 | return (testEnvironmentIsSignedOrDoesNotRequireEntitlement() 29 | ? Valet.iCloudPermutations(with: CloudIntegrationTests.identifier.asIdentifier) + Valet.iCloudPermutations(with: CloudIntegrationTests.identifier) 30 | : []) 31 | } 32 | let key = "key" 33 | let passcode = "topsecret" 34 | 35 | override func setUp() 36 | { 37 | super.setUp() 38 | 39 | allPermutations.forEach { testValet in 40 | do { 41 | try testValet.removeAllObjects() 42 | } catch { 43 | XCTFail("Error removing objects from Valet \(testValet): \(error)") 44 | } 45 | } 46 | } 47 | 48 | func test_synchronizableValet_isDistinctFromVanillaValetWithEqualConfiguration() throws 49 | { 50 | guard testEnvironmentIsSignedOrDoesNotRequireEntitlement() else { 51 | return 52 | } 53 | 54 | let identifier = Identifier(nonEmpty: "DistinctTest")! 55 | let vanillaValet = Valet.valet(with: identifier, accessibility: .afterFirstUnlock) 56 | let iCloudValet = Valet.iCloudValet(with: identifier, accessibility: .afterFirstUnlock) 57 | 58 | // Setting 59 | try iCloudValet.setString("butts", forKey: "cloud") 60 | XCTAssertEqual("butts", try iCloudValet.string(forKey: "cloud")) 61 | XCTAssertThrowsError(try vanillaValet.string(forKey: "cloud")) { error in 62 | XCTAssertEqual(error as? KeychainError, .itemNotFound) 63 | } 64 | 65 | // Removal 66 | try vanillaValet.setString("snake people", forKey: "millennials") 67 | try iCloudValet.removeObject(forKey: "millennials") 68 | XCTAssertEqual("snake people", try vanillaValet.string(forKey: "millennials")) 69 | } 70 | 71 | func test_setStringForKey() throws 72 | { 73 | try allPermutations.forEach { valet in 74 | XCTAssertThrowsError(try valet.string(forKey: key)) { error in 75 | XCTAssertEqual(error as? KeychainError, .itemNotFound) 76 | } 77 | try valet.setString(passcode, forKey: key) 78 | XCTAssertEqual(passcode, try valet.string(forKey: key)) 79 | } 80 | } 81 | 82 | func test_removeObjectForKey() throws 83 | { 84 | try allPermutations.forEach { valet in 85 | try valet.setString(passcode, forKey: key) 86 | XCTAssertEqual(passcode, try valet.string(forKey: key)) 87 | 88 | try valet.removeObject(forKey: key) 89 | XCTAssertThrowsError(try valet.string(forKey: key)) { error in 90 | XCTAssertEqual(error as? KeychainError, .itemNotFound) 91 | } 92 | } 93 | } 94 | 95 | // MARK: canAccessKeychain 96 | 97 | func test_canAccessKeychain() 98 | { 99 | allPermutations.forEach { valet in 100 | XCTAssertTrue(valet.canAccessKeychain(), "\(valet) could not access keychain.") 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /ValetTouchIDTest/ValetTouchIDTestViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Eric Muller on 4/20/16. 2 | // Copyright © 2016 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Valet 18 | import UIKit 19 | 20 | 21 | final class ValetTouchIDTestViewController : UIViewController 22 | { 23 | // MARK: Properties 24 | 25 | @IBOutlet var textView : UITextView? 26 | var singlePromptSecureEnclaveValet : SinglePromptSecureEnclaveValet 27 | let username = "CustomerPresentProof" 28 | 29 | // MARK: Initializers 30 | 31 | required init?(coder aDecoder: NSCoder) 32 | { 33 | singlePromptSecureEnclaveValet = SinglePromptSecureEnclaveValet.valet(with: Identifier(nonEmpty: "UserPresence")!, accessControl: .userPresence) 34 | 35 | super.init(coder: aDecoder) 36 | } 37 | 38 | // MARK: Outlets 39 | 40 | @objc(setOrUpdateItem:) 41 | @IBAction func setOrUpdateItem(sender: UIResponder) 42 | { 43 | let stringToSet = "I am here! " + NSUUID().uuidString 44 | let setOrUpdatedItem: Bool 45 | do { 46 | try singlePromptSecureEnclaveValet.setString(stringToSet, forKey: username) 47 | setOrUpdatedItem = true 48 | } catch { 49 | setOrUpdatedItem = false 50 | } 51 | updateTextView(messageComponents: #function, (setOrUpdatedItem ? "Success" : "Failure")) 52 | } 53 | 54 | @objc(getItem:) 55 | @IBAction func getItem(sender: UIResponder) 56 | { 57 | let resultString: String 58 | do { 59 | resultString = try singlePromptSecureEnclaveValet.string(forKey: username, withPrompt: "Use TouchID to retrieve password") 60 | } catch KeychainError.userCancelled { 61 | resultString = "user cancelled TouchID" 62 | } catch KeychainError.itemNotFound { 63 | resultString = "object not found" 64 | } catch { 65 | resultString = "caught unknown error \(error)" 66 | } 67 | 68 | updateTextView(messageComponents: #function, resultString) 69 | } 70 | 71 | @objc(removeItem:) 72 | @IBAction func removeItem(sender: UIResponder) 73 | { 74 | let removedItem: Bool 75 | do { 76 | try singlePromptSecureEnclaveValet.removeObject(forKey: username) 77 | removedItem = true 78 | } catch { 79 | removedItem = false 80 | } 81 | 82 | updateTextView(messageComponents: #function, (removedItem ? "Success" : "Failure")) 83 | } 84 | 85 | @objc(containsItem:) 86 | @IBAction func containsItem(sender: UIResponder) 87 | { 88 | let containsItem: Bool 89 | do { 90 | containsItem = try singlePromptSecureEnclaveValet.containsObject(forKey: username) 91 | } catch { 92 | containsItem = false 93 | } 94 | updateTextView(messageComponents: #function, (containsItem ? "YES" : "NO")) 95 | } 96 | 97 | @objc(requirePrompt:) 98 | @IBAction func requirePrompt(sender: UIResponder) 99 | { 100 | singlePromptSecureEnclaveValet.requirePromptOnNextAccess() 101 | updateTextView(messageComponents: #function) 102 | } 103 | 104 | // MARK: Private 105 | 106 | private func updateTextView(messageComponents: String...) 107 | { 108 | if let textView = textView { 109 | textView.text = textView.text.appendingFormat("\n%@", messageComponents.joined(separator: " ")) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ValetTouchIDTest/Base.lproj/ValetSecureElementTestLaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Sources/Valet/Accessibility.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman and Eric Muller on 9/16/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | 20 | @objc(VALAccessibility) 21 | public enum Accessibility: Int, CaseIterable, CustomStringConvertible, Equatable, Sendable { 22 | /// Valet data can only be accessed while the device is unlocked. This attribute is recommended for data that only needs to be accessible while the application is in the foreground. Valet data with this attribute will migrate to a new device when using encrypted backups. 23 | case whenUnlocked = 1 24 | /// Valet data cannot be accessed after a restart until the device has been unlocked once; data is accessible until the device is next rebooted. This attribute is recommended for data that needs to be accessible by background applications. Valet data with this attribute will migrate to a new device when using encrypted backups. 25 | case afterFirstUnlock = 2 26 | 27 | /// Valet data can only be accessed while the device is unlocked. This attribute is recommended for items that only need to be accessible while the application is in the foreground. Valet data with this attribute will never migrate to a new device, so these items will be missing after a backup is restored to a new device. No items can be stored in this class on devices without a passcode. Disabling the device passcode will cause all items in this class to be deleted. 28 | case whenPasscodeSetThisDeviceOnly = 4 29 | /// Valet data can only be accessed while the device is unlocked. This is recommended for data that only needs to be accessible while the application is in the foreground. Valet data with this attribute will never migrate to a new device, so these items will be missing after a backup is restored to a new device. 30 | case whenUnlockedThisDeviceOnly = 5 31 | /// Valet data cannot be accessed after a restart until the device has been unlocked once; data is accessible until the device is next rebooted. This attribute is recommended for data that needs to be accessible by background applications. Valet data with this attribute will never migrate to a new device, so these items will be missing after a backup is restored to a new device. 32 | case afterFirstUnlockThisDeviceOnly = 6 33 | 34 | // MARK: CustomStringConvertible 35 | 36 | public var description: String { 37 | switch self { 38 | case .afterFirstUnlock: 39 | return "AccessibleAfterFirstUnlock" 40 | case .afterFirstUnlockThisDeviceOnly: 41 | return "AccessibleAfterFirstUnlockThisDeviceOnly" 42 | case .whenPasscodeSetThisDeviceOnly: 43 | return "AccessibleWhenPasscodeSetThisDeviceOnly" 44 | case .whenUnlocked: 45 | return "AccessibleWhenUnlocked" 46 | case .whenUnlockedThisDeviceOnly: 47 | return "AccessibleWhenUnlockedThisDeviceOnly" 48 | } 49 | } 50 | 51 | // MARK: Public Properties 52 | 53 | public var secAccessibilityAttribute: String { 54 | let accessibilityAttribute: CFString 55 | 56 | switch self { 57 | case .afterFirstUnlock: 58 | accessibilityAttribute = kSecAttrAccessibleAfterFirstUnlock 59 | case .afterFirstUnlockThisDeviceOnly: 60 | accessibilityAttribute = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 61 | case .whenPasscodeSetThisDeviceOnly: 62 | accessibilityAttribute = kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly 63 | case .whenUnlocked: 64 | accessibilityAttribute = kSecAttrAccessibleWhenUnlocked 65 | case .whenUnlockedThisDeviceOnly: 66 | accessibilityAttribute = kSecAttrAccessibleWhenUnlockedThisDeviceOnly 67 | } 68 | 69 | return accessibilityAttribute as String 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Valet/MigratableKeyValuePair.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 5/20/20. 2 | // Copyright © 2020 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | /// A struct that represented a key:value pair that can be migrated. 20 | public struct MigratableKeyValuePair: Hashable { 21 | 22 | // MARK: Initialization 23 | 24 | /// Creates a migratable key:value pair with the provided inputs. 25 | /// - Parameters: 26 | /// - key: The key in the key:value pair. 27 | /// - value: The value in the key:value pair. 28 | public init(key: Key, value: Data) { 29 | self.key = key 30 | self.value = value 31 | } 32 | 33 | /// Creates a migratable key:value pair with the provided inputs. 34 | /// - Parameters: 35 | /// - key: The key in the key:value pair. 36 | /// - value: The desired value in the key:value pair, represented as a String. 37 | public init(key: Key, value: String) { 38 | self.key = key 39 | self.value = Data(value.utf8) 40 | } 41 | 42 | // MARK: Public 43 | 44 | /// The key in the key:value pair. 45 | public let key: Key 46 | /// The value in the key:value pair. 47 | public let value: Data 48 | } 49 | 50 | // MARK: - Objective-C Compatibility 51 | 52 | @objc(VALMigratableKeyValuePairInput) 53 | public final class ObjectiveCCompatibilityMigratableKeyValuePairInput: NSObject { 54 | 55 | // MARK: Initialization 56 | 57 | init(key: Any, value: Data) { 58 | self.key = key 59 | self.value = value 60 | } 61 | 62 | // MARK: Public 63 | 64 | /// The key in the key:value pair. 65 | @objc 66 | public let key: Any 67 | /// The value in the key:value pair. 68 | @objc 69 | public let value: Data 70 | } 71 | 72 | @objc(VALMigratableKeyValuePairOutput) 73 | public class ObjectiveCCompatibilityMigratableKeyValuePairOutput: NSObject { 74 | 75 | // MARK: Initialization 76 | 77 | /// Creates a migratable key:value pair with the provided inputs. 78 | /// - Parameters: 79 | /// - key: The key in the key:value pair. 80 | /// - value: The value in the key:value pair. 81 | @objc 82 | public init(key: String, value: Data) { 83 | self.key = key 84 | self.value = value 85 | preventMigration = false 86 | } 87 | 88 | /// Creates a migratable key:value pair with the provided inputs. 89 | /// - Parameters: 90 | /// - key: The key in the key:value pair. 91 | /// - stringValue: The desired value in the key:value pair, represented as a String. 92 | @objc 93 | public init(key: String, stringValue: String) { 94 | self.key = key 95 | self.value = Data(stringValue.utf8) 96 | preventMigration = false 97 | } 98 | 99 | // MARK: Public Static Methods 100 | 101 | /// A sentinal `ObjectiveCCompatibilityMigratableKeyValuePairOutput` that conveys that the migration should be prevented. 102 | @available(swift, obsoleted: 1.0) 103 | @objc 104 | public static func preventMigration() -> ObjectiveCCompatibilityMigratableKeyValuePairOutput { 105 | ObjectiveCCompatibilityPreventMigrationOutput() 106 | } 107 | 108 | // MARK: Public 109 | 110 | /// The key in the key:value pair. 111 | @objc 112 | public let key: String 113 | /// The value in the key:value pair. 114 | @objc 115 | public let value: Data 116 | 117 | // MARK: Internal 118 | 119 | fileprivate(set) var preventMigration: Bool 120 | 121 | } 122 | 123 | private final class ObjectiveCCompatibilityPreventMigrationOutput: ObjectiveCCompatibilityMigratableKeyValuePairOutput { 124 | 125 | init() { 126 | super.init(key: "", stringValue: "") 127 | preventMigration = true 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /Valet.xcodeproj/xcshareddata/xcschemes/Valet watchOS Test Host App.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 66 | 68 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Valet.xcodeproj/xcshareddata/xcschemes/Valet tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 77 | 83 | 84 | 85 | 86 | 92 | 93 | 99 | 100 | 101 | 102 | 104 | 105 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '35 2 * * 0' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | runs-on: macOS-15 26 | permissions: 27 | # required for all workflows 28 | security-events: write 29 | 30 | # required to fetch internal or private CodeQL packs 31 | packages: read 32 | 33 | # only required for workflows in private repositories 34 | actions: read 35 | contents: read 36 | 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | include: 41 | - language: actions 42 | build-mode: none 43 | - language: ruby 44 | build-mode: none 45 | - language: swift 46 | build-mode: autobuild 47 | # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' 48 | # Use `c-cpp` to analyze code written in C, C++ or both 49 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 50 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 51 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 52 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 53 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 54 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 55 | steps: 56 | - name: Checkout repository 57 | uses: actions/checkout@v5 58 | - name: Select Xcode Version 59 | run: sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer 60 | 61 | # Add any setup steps before running the `github/codeql-action/init` action. 62 | # This includes steps like installing compilers or runtimes (`actions/setup-node` 63 | # or others). This is typically only required for manual builds. 64 | # - name: Setup runtime (example) 65 | # uses: actions/setup-example@v1 66 | 67 | # Initializes the CodeQL tools for scanning. 68 | - name: Initialize CodeQL 69 | uses: github/codeql-action/init@v4 70 | with: 71 | languages: ${{ matrix.language }} 72 | build-mode: ${{ matrix.build-mode }} 73 | # If you wish to specify custom queries, you can do so here or in a config file. 74 | # By default, queries listed here will override any specified in a config file. 75 | # Prefix the list here with "+" to use these queries and those in the config file. 76 | 77 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 78 | # queries: security-extended,security-and-quality 79 | 80 | # If the analyze step fails for one of the languages you are analyzing with 81 | # "We were unable to automatically build your code", modify the matrix above 82 | # to set the build mode to "manual" for that language. Then modify this step 83 | # to build your code. 84 | # ℹ️ Command-line programs to run using the OS shell. 85 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 86 | - if: matrix.build-mode == 'manual' 87 | shell: bash 88 | run: | 89 | echo 'If you are using a "manual" build mode for one or more of the' \ 90 | 'languages you are analyzing, replace this with the commands to build' \ 91 | 'your code, for example:' 92 | echo ' make bootstrap' 93 | echo ' make release' 94 | exit 1 95 | 96 | - name: Perform CodeQL Analysis 97 | uses: github/codeql-action/analyze@v4 98 | with: 99 | category: "/language:${{matrix.language}}" 100 | -------------------------------------------------------------------------------- /Valet.xcodeproj/xcshareddata/xcschemes/Valet watchOS Test Host App Extension.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 65 | 66 | 67 | 68 | 69 | 70 | 80 | 82 | 88 | 89 | 90 | 91 | 97 | 99 | 105 | 106 | 107 | 108 | 110 | 111 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /Sources/Valet/Internal/SecItem.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman and Eric Muller on 9/16/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | 20 | func execute(in lock: NSLock, block: () throws -> ReturnType) rethrows -> ReturnType { 21 | lock.lock() 22 | defer { 23 | lock.unlock() 24 | } 25 | return try block() 26 | } 27 | 28 | 29 | final class SecItem { 30 | 31 | // MARK: Internal Class Methods 32 | 33 | static func copy(matching query: [String : AnyHashable]) throws(KeychainError) -> DesiredType { 34 | if query.isEmpty { 35 | assertionFailure("Must provide a query with at least one item") 36 | } 37 | 38 | var status = errSecNotAvailable 39 | var result: AnyObject? = nil 40 | execute(in: secItemLock) { 41 | status = SecItemCopyMatching(query as CFDictionary, &result) 42 | } 43 | 44 | if status == errSecSuccess { 45 | if let result = result as? DesiredType { 46 | return result 47 | 48 | } else { 49 | // The query failed to pull out a value object of the desired type, but did find metadata matching this query. 50 | // This can happen because either the query didn't ask for return data via [kSecReturnData : true], or because a metadata-only item existed in the keychain. 51 | throw KeychainError.itemNotFound 52 | } 53 | 54 | } else { 55 | throw KeychainError(status: status) 56 | } 57 | } 58 | 59 | static func performCopy(matching query: [String : AnyHashable]) -> OSStatus { 60 | guard !query.isEmpty else { 61 | // Must provide a query with at least one item 62 | return errSecParam 63 | } 64 | 65 | var status = errSecNotAvailable 66 | execute(in: secItemLock) { 67 | status = SecItemCopyMatching(query as CFDictionary, nil) 68 | } 69 | 70 | return status 71 | } 72 | 73 | static func add(attributes: [String : AnyHashable]) throws(KeychainError) { 74 | if attributes.isEmpty { 75 | assertionFailure("Must provide attributes with at least one item") 76 | } 77 | 78 | var status = errSecNotAvailable 79 | var result: AnyObject? = nil 80 | execute(in: secItemLock) { 81 | status = SecItemAdd(attributes as CFDictionary, &result) 82 | } 83 | 84 | switch status { 85 | case errSecSuccess: 86 | // We're done! 87 | break 88 | default: 89 | throw KeychainError(status: status) 90 | } 91 | } 92 | 93 | static func update(attributes: [String : AnyHashable], forItemsMatching query: [String : AnyHashable]) throws(KeychainError) { 94 | if attributes.isEmpty { 95 | assertionFailure("Must provide attributes with at least one item") 96 | } 97 | 98 | if query.isEmpty { 99 | assertionFailure("Must provide a query with at least one item") 100 | } 101 | 102 | var status = errSecNotAvailable 103 | execute(in: secItemLock) { 104 | status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) 105 | } 106 | 107 | switch status { 108 | case errSecSuccess: 109 | // We're done! 110 | break 111 | default: 112 | throw KeychainError(status: status) 113 | } 114 | } 115 | 116 | static func deleteItems(matching query: [String : AnyHashable]) throws(KeychainError) { 117 | if query.isEmpty { 118 | assertionFailure("Must provide a query with at least one item") 119 | } 120 | 121 | var secItemQuery = query 122 | #if os(macOS) 123 | // This line must exist on OS X, but must not exist on iOS. 124 | secItemQuery[kSecMatchLimit as String] = kSecMatchLimitAll 125 | #endif 126 | var status = errSecNotAvailable 127 | execute(in: secItemLock) { 128 | status = SecItemDelete(secItemQuery as CFDictionary) 129 | } 130 | 131 | if status == errSecSuccess { 132 | // We're done! 133 | 134 | } else { 135 | switch KeychainError(status: status) { 136 | case .couldNotAccessKeychain: 137 | throw KeychainError.couldNotAccessKeychain 138 | 139 | case .missingEntitlement: 140 | throw KeychainError.missingEntitlement 141 | 142 | case .emptyKey, 143 | .emptyValue, 144 | .itemNotFound, 145 | .userCancelled: 146 | // We succeeded as long as we can confirm that the item is not in the keychain. 147 | break 148 | } 149 | } 150 | } 151 | 152 | // MARK: Private Properties 153 | 154 | private static let secItemLock = NSLock() 155 | } 156 | -------------------------------------------------------------------------------- /Scripts/build.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env swift 2 | 3 | import Foundation 4 | 5 | // Usage: build.swift platforms [spm|xcode] 6 | 7 | func execute(commandPath: String, arguments: [String]) throws { 8 | let task = Process() 9 | task.executableURL = .init(filePath: commandPath) 10 | task.arguments = arguments 11 | print("Launching command: \(commandPath) \(arguments.joined(separator: " "))") 12 | try task.run() 13 | task.waitUntilExit() 14 | guard task.terminationStatus == 0 else { 15 | throw TaskError.code(task.terminationStatus) 16 | } 17 | } 18 | 19 | enum TaskError: Error { 20 | case code(Int32) 21 | } 22 | 23 | enum Platform: String, CustomStringConvertible { 24 | case iOS_18 25 | case tvOS_18 26 | case macOS_15 27 | case watchOS_11 28 | 29 | var destination: String { 30 | switch self { 31 | case .iOS_18: 32 | "platform=iOS Simulator,OS=18.4,name=iPad (10th generation)" 33 | 34 | case .tvOS_18: 35 | "platform=tvOS Simulator,OS=18.2,name=Apple TV" 36 | 37 | case .macOS_15: 38 | "platform=OS X" 39 | 40 | case .watchOS_11: 41 | "OS=11.2,name=Apple Watch Series 10 (46mm)" 42 | } 43 | } 44 | 45 | var sdk: String { 46 | switch self { 47 | case .iOS_18: 48 | "iphonesimulator" 49 | 50 | case .tvOS_18: 51 | "appletvsimulator" 52 | 53 | case .macOS_15: 54 | "macosx15.5" 55 | 56 | case .watchOS_11: 57 | "watchsimulator" 58 | } 59 | } 60 | 61 | var derivedDataPath: String { 62 | ".build/derivedData/" + description 63 | } 64 | 65 | var scheme: String { 66 | switch self { 67 | case .iOS_18: 68 | "Valet iOS" 69 | 70 | case .tvOS_18: 71 | "Valet tvOS" 72 | 73 | case .macOS_15: 74 | "Valet Mac" 75 | 76 | case .watchOS_11: 77 | "Valet watchOS" 78 | } 79 | } 80 | 81 | var description: String { 82 | rawValue 83 | } 84 | } 85 | 86 | enum Task: String, CustomStringConvertible { 87 | case spm 88 | case xcode 89 | 90 | var description: String { 91 | rawValue 92 | } 93 | 94 | var shouldUseLegacyBuildSystem: Bool { 95 | switch self { 96 | case .spm: 97 | false 98 | case .xcode: 99 | // The new build system choked on our XCTest framework. 100 | // Once this project compiles with the new build system, 101 | // we can change this to false. 102 | true 103 | } 104 | } 105 | 106 | var configuration: String { 107 | switch self { 108 | case .spm: 109 | "Release" 110 | case .xcode: 111 | "Debug" 112 | } 113 | } 114 | 115 | func scheme(for platform: Platform) -> String { 116 | switch self { 117 | case .spm: 118 | "Valet" 119 | case .xcode: 120 | platform.scheme 121 | } 122 | } 123 | 124 | func shouldTest(on platform: Platform) -> Bool { 125 | switch self { 126 | case .spm: 127 | // Our Package isn't set up with unit test targets, because SPM can't run unit tests in a codesigned environment. 128 | false 129 | case .xcode: 130 | true 131 | } 132 | } 133 | } 134 | 135 | guard CommandLine.arguments.count > 2 else { 136 | print("Usage: build.swift platforms [spm|xcode]") 137 | throw TaskError.code(1) 138 | } 139 | let rawPlatforms = CommandLine.arguments[1].components(separatedBy: ",") 140 | let rawTask = CommandLine.arguments[2] 141 | 142 | guard let task = Task(rawValue: rawTask) else { 143 | print("Received unknown task \(rawTask)") 144 | throw TaskError.code(1) 145 | } 146 | 147 | let platforms = try rawPlatforms.map { rawPlatform -> Platform in 148 | guard let platform = Platform(rawValue: rawPlatform) else { 149 | print("Received unknown platform type \(rawPlatform)") 150 | throw TaskError.code(1) 151 | } 152 | 153 | return platform 154 | } 155 | 156 | for platform in platforms { 157 | var deletedXcodeproj = false 158 | var xcodeBuildArguments: [String] = [] 159 | // If necessary, delete Valet.xcodeproj, otherwise xcodebuild won't generate the SPM scheme. 160 | // If deleted, the xcodeproj will be restored by git at the end of the loop. 161 | if task == .spm { 162 | do { 163 | print("Deleting Valet.xcodeproj, any uncommitted changes will be lost.") 164 | try execute(commandPath: "/bin/rm", arguments: ["-r", "Valet.xcodeproj"]) 165 | deletedXcodeproj = true 166 | } catch { 167 | print("Could not delete Valet.xcodeproj due to error: \(error)") 168 | throw TaskError.code(1) 169 | } 170 | } 171 | 172 | xcodeBuildArguments.append(contentsOf: [ 173 | "-scheme", task.scheme(for: platform), 174 | "-sdk", platform.sdk, 175 | "-configuration", task.configuration, 176 | "-PBXBuildsContinueAfterErrors=0", 177 | ]) 178 | if !platform.destination.isEmpty { 179 | xcodeBuildArguments.append("-destination") 180 | xcodeBuildArguments.append(platform.destination) 181 | } 182 | if task.shouldUseLegacyBuildSystem { 183 | xcodeBuildArguments.append("-UseModernBuildSystem=0") 184 | } 185 | let shouldTest = task.shouldTest(on: platform) 186 | if shouldTest { 187 | xcodeBuildArguments.append("-enableCodeCoverage") 188 | xcodeBuildArguments.append("YES") 189 | xcodeBuildArguments.append("-derivedDataPath") 190 | xcodeBuildArguments.append(platform.derivedDataPath) 191 | } 192 | xcodeBuildArguments.append("build") 193 | if shouldTest { 194 | xcodeBuildArguments.append("test") 195 | } 196 | 197 | try execute(commandPath: "/usr/bin/xcodebuild", arguments: xcodeBuildArguments) 198 | 199 | if deletedXcodeproj { 200 | do { 201 | print("Restoring Valet.xcodeproj") 202 | try execute(commandPath: "/usr/bin/git", arguments: ["restore", "Valet.xcodeproj"]) 203 | } catch { 204 | print("Failed to reset Valet.xcodeproj to last committed version due to error: \(error)") 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /Sources/Valet/Internal/Service.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman and Eric Muller on 9/16/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | 20 | enum Service: CustomStringConvertible, Equatable, Sendable { 21 | case standard(Identifier, Configuration) 22 | case sharedGroup(SharedGroupIdentifier, Identifier?, Configuration) 23 | 24 | #if os(macOS) 25 | case standardOverride(service: Identifier, Configuration) 26 | case sharedGroupOverride(service: SharedGroupIdentifier, Configuration) 27 | #endif 28 | 29 | // MARK: Equatable 30 | 31 | static func ==(lhs: Service, rhs: Service) -> Bool { 32 | lhs.description == rhs.description 33 | } 34 | 35 | // MARK: CustomStringConvertible 36 | 37 | var description: String { 38 | secService 39 | } 40 | 41 | // MARK: Internal Static Methods 42 | 43 | static func standard(with configuration: Configuration, identifier: Identifier, accessibilityDescription: String) -> String { 44 | "VAL_\(configuration.description)_initWithIdentifier:accessibility:_\(identifier)_\(accessibilityDescription)" 45 | } 46 | 47 | static func sharedGroup(with configuration: Configuration, groupIdentifier: SharedGroupIdentifier, identifier: Identifier?, accessibilityDescription: String) -> String { 48 | if let identifier = identifier { 49 | return "VAL_\(configuration.description)_initWithSharedAccessGroupIdentifier:accessibility:_\(groupIdentifier.groupIdentifier)_\(identifier)_\(accessibilityDescription)" 50 | } else { 51 | return "VAL_\(configuration.description)_initWithSharedAccessGroupIdentifier:accessibility:_\(groupIdentifier.groupIdentifier)_\(accessibilityDescription)" 52 | } 53 | } 54 | 55 | static func sharedGroup(with configuration: Configuration, explicitlySetIdentifier identifier: Identifier, accessibilityDescription: String) -> String { 56 | "VAL_\(configuration.description)_initWithSharedAccessGroupIdentifier:accessibility:_\(identifier)_\(accessibilityDescription)" 57 | } 58 | 59 | // MARK: Internal Methods 60 | 61 | func generateBaseQuery() -> [String : AnyHashable] { 62 | var baseQuery: [String : AnyHashable] = [ 63 | kSecClass as String : kSecClassGenericPassword as String, 64 | kSecAttrService as String : secService, 65 | ] 66 | 67 | if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) { 68 | baseQuery[kSecUseDataProtectionKeychain as String] = true 69 | } 70 | 71 | let configuration: Configuration 72 | switch self { 73 | case let .standard(_, desiredConfiguration): 74 | configuration = desiredConfiguration 75 | 76 | case let .sharedGroup(groupIdentifier, _, desiredConfiguration): 77 | baseQuery[kSecAttrAccessGroup as String] = groupIdentifier.description 78 | configuration = desiredConfiguration 79 | 80 | #if os(macOS) 81 | case let .standardOverride(_, desiredConfiguration): 82 | configuration = desiredConfiguration 83 | 84 | case let .sharedGroupOverride(identifier, desiredConfiguration): 85 | baseQuery[kSecAttrAccessGroup as String] = identifier.description 86 | configuration = desiredConfiguration 87 | #endif 88 | } 89 | 90 | switch configuration { 91 | case .valet: 92 | baseQuery[kSecAttrAccessible as String] = configuration.accessibility.secAccessibilityAttribute 93 | 94 | case .iCloud: 95 | baseQuery[kSecAttrSynchronizable as String] = true 96 | baseQuery[kSecAttrAccessible as String] = configuration.accessibility.secAccessibilityAttribute 97 | 98 | case let .secureEnclave(desiredAccessControl), 99 | let .singlePromptSecureEnclave(desiredAccessControl): 100 | // Note that kSecAttrAccessControl and kSecAttrAccessible are mutually exclusive. 101 | baseQuery[kSecAttrAccessControl as String] = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, desiredAccessControl.secAccessControl, nil) 102 | } 103 | 104 | return baseQuery 105 | } 106 | 107 | // MARK: Private Methods 108 | 109 | private var secService: String { 110 | var service: String 111 | switch self { 112 | case let .standard(identifier, configuration): 113 | service = Service.standard(with: configuration, identifier: identifier, accessibilityDescription: configuration.accessibility.description) 114 | case let .sharedGroup(groupIdentifier, identifier, configuration): 115 | service = Service.sharedGroup(with: configuration, groupIdentifier: groupIdentifier, identifier: identifier, accessibilityDescription: configuration.accessibility.description) 116 | #if os(macOS) 117 | case let .standardOverride(identifier, _): 118 | service = identifier.description 119 | case let .sharedGroupOverride(identifier, _): 120 | service = identifier.groupIdentifier 121 | #endif 122 | } 123 | 124 | switch self { 125 | case let .standard(_, configuration), 126 | let .sharedGroup(_, _, configuration): 127 | switch configuration { 128 | case .valet, .iCloud: 129 | // Nothing to do here. 130 | break 131 | 132 | case let .secureEnclave(accessControl), 133 | let .singlePromptSecureEnclave(accessControl): 134 | service += accessControl.description 135 | } 136 | 137 | return service 138 | 139 | #if os(macOS) 140 | case .standardOverride, 141 | .sharedGroupOverride: 142 | return service 143 | #endif 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Tests/ValetTests/ValetTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Eric Muller on 4/25/16. 2 | // Copyright © 2016 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | import XCTest 19 | 20 | @testable import Valet 21 | 22 | 23 | class ValetTests: XCTestCase 24 | { 25 | static let identifier = Identifier(nonEmpty: "valet_testing")! 26 | let valet = Valet.valet(with: identifier, accessibility: .whenUnlocked) 27 | 28 | // MARK: Initialization 29 | 30 | func test_init_createsCorrectBackingService() { 31 | let identifier = ValetTests.identifier 32 | 33 | Accessibility.allCases.forEach { accessibility in 34 | let backingService = Valet.valet(with: identifier, accessibility: accessibility).service 35 | XCTAssertEqual(backingService, Service.standard(identifier, .valet(accessibility))) 36 | } 37 | } 38 | 39 | func test_init_createsCorrectBackingService_sharedAccess() { 40 | let identifier = Valet.sharedAccessGroupIdentifier 41 | 42 | Accessibility.allCases.forEach { accessibility in 43 | let backingService = Valet.sharedGroupValet(with: identifier, accessibility: accessibility).service 44 | XCTAssertEqual(backingService, Service.sharedGroup(identifier, nil, .valet(accessibility))) 45 | } 46 | } 47 | 48 | func test_init_createsCorrectBackingService_sharedAccess_withIdentifier() { 49 | let groupIdentifier = Valet.sharedAccessGroupIdentifier 50 | let identifier = Identifier(nonEmpty: "UniquenessIdentifier") 51 | 52 | Accessibility.allCases.forEach { accessibility in 53 | let backingService = Valet.sharedGroupValet(with: groupIdentifier, identifier: identifier, accessibility: accessibility).service 54 | XCTAssertEqual(backingService, Service.sharedGroup(groupIdentifier, identifier, .valet(accessibility))) 55 | } 56 | } 57 | 58 | func test_init_createsCorrectBackingService_cloud() { 59 | let identifier = ValetTests.identifier 60 | 61 | CloudAccessibility.allCases.forEach { accessibility in 62 | let backingService = Valet.iCloudValet(with: identifier, accessibility: accessibility).service 63 | XCTAssertEqual(backingService, Service.standard(identifier, .iCloud(accessibility))) 64 | } 65 | } 66 | 67 | func test_init_createsCorrectBackingService_cloudSharedAccess() { 68 | let identifier = Valet.sharedAccessGroupIdentifier 69 | 70 | CloudAccessibility.allCases.forEach { accessibility in 71 | let backingService = Valet.iCloudSharedGroupValet(with: identifier, accessibility: accessibility).service 72 | XCTAssertEqual(backingService, Service.sharedGroup(identifier, nil, .iCloud(accessibility))) 73 | } 74 | } 75 | 76 | // MARK: Equality 77 | 78 | func test_valetsWithSameConfiguration_areEqual() 79 | { 80 | let equalValet = Valet.valet(with: valet.identifier, accessibility: valet.accessibility) 81 | XCTAssertTrue(equalValet == valet) 82 | XCTAssertTrue(equalValet === valet) 83 | } 84 | 85 | func test_differentValetFlavorsWithEquivalentConfiguration_areNotEqual() 86 | { 87 | let anotherFlavor = Valet.iCloudValet(with: ValetTests.identifier, accessibility: .whenUnlocked) 88 | XCTAssertFalse(valet == anotherFlavor) 89 | XCTAssertFalse(valet === anotherFlavor) 90 | } 91 | 92 | func test_valetsWithDifferingIdentifier_areNotEqual() 93 | { 94 | let differingIdentifier = Valet.valet(with: Identifier(nonEmpty: "nope")!, accessibility: valet.accessibility) 95 | XCTAssertNotEqual(valet, differingIdentifier) 96 | } 97 | 98 | func test_valetsWithDifferingAccessibility_areNotEqual() 99 | { 100 | let differingAccessibility = Valet.valet(with: valet.identifier, accessibility: .whenUnlockedThisDeviceOnly) 101 | XCTAssertNotEqual(valet, differingAccessibility) 102 | } 103 | 104 | // MARK: Migration - Query 105 | 106 | func test_migrateObjectsMatching_failsForBadQueries() 107 | { 108 | XCTAssertThrowsError(try valet.migrateObjects(matching: [:], removeOnCompletion: false)) { error in 109 | XCTAssertEqual(error as? MigrationError, .invalidQuery) 110 | } 111 | XCTAssertThrowsError(try valet.migrateObjects(matching: [:], removeOnCompletion: true)) { error in 112 | XCTAssertEqual(error as? MigrationError, .invalidQuery) 113 | } 114 | 115 | var invalidQuery: [String: AnyHashable] = [ 116 | kSecClass as String: kSecClassGenericPassword, 117 | kSecMatchLimit as String: kSecMatchLimitOne 118 | ] 119 | // Migration queries should have kSecMatchLimit set to .All 120 | XCTAssertThrowsError(try valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) { error in 121 | XCTAssertEqual(error as? MigrationError, .invalidQuery) 122 | } 123 | 124 | invalidQuery = [ 125 | kSecClass as String: kSecClassGenericPassword, 126 | kSecReturnData as String: kCFBooleanTrue 127 | ] 128 | // Migration queries do not support kSecReturnData 129 | XCTAssertThrowsError(try valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) { error in 130 | XCTAssertEqual(error as? MigrationError, .invalidQuery) 131 | } 132 | 133 | invalidQuery = [ 134 | kSecClass as String: kSecClassGenericPassword, 135 | kSecReturnRef as String: kCFBooleanTrue 136 | ] 137 | // Migration queries do not support kSecReturnRef 138 | XCTAssertThrowsError(try valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) { error in 139 | XCTAssertEqual(error as? MigrationError, .invalidQuery) 140 | } 141 | 142 | invalidQuery = [ 143 | kSecClass as String: kSecClassGenericPassword, 144 | kSecReturnPersistentRef as String: kCFBooleanFalse 145 | ] 146 | // Migration queries must have kSecReturnPersistentRef set to true 147 | XCTAssertThrowsError(try valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) { error in 148 | XCTAssertEqual(error as? MigrationError, .invalidQuery) 149 | } 150 | 151 | 152 | invalidQuery = [ 153 | kSecClass as String: kSecClassGenericPassword, 154 | kSecReturnAttributes as String: kCFBooleanFalse 155 | ] 156 | // Migration queries must have kSecReturnAttributes set to true 157 | XCTAssertThrowsError(try valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) { error in 158 | XCTAssertEqual(error as? MigrationError, .invalidQuery) 159 | } 160 | 161 | invalidQuery = [ 162 | kSecClass as String: kSecClassGenericPassword, 163 | kSecAttrAccessControl as String: NSNull() 164 | ] 165 | // Migration queries must not have kSecAttrAccessControl set 166 | XCTAssertThrowsError(try valet.migrateObjects(matching: invalidQuery, removeOnCompletion: false)) { error in 167 | XCTAssertEqual(error as? MigrationError, .invalidQuery) 168 | } 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /Sources/Valet/SecureEnclave.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 9/19/17. 2 | // Copyright © 2017 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #if !os(tvOS) && canImport(LocalAuthentication) 18 | import LocalAuthentication 19 | #endif 20 | import Foundation 21 | 22 | 23 | public final class SecureEnclave: Sendable { 24 | 25 | // MARK: Internal Methods 26 | 27 | /// - Parameter service: The service of the keychain slice we want to check if we can access. 28 | /// - Returns: `true` if the keychain is accessible for reading and writing, `false` otherwise. 29 | /// - Note: Determined by writing a value to the keychain and then reading it back out. 30 | static func canAccessKeychain(with service: Service) -> Bool { 31 | // To avoid prompting the user for Touch ID or passcode, create a Valet with our identifier and accessibility and ask it if it can access the keychain. 32 | let noPromptValet: Valet 33 | switch service { 34 | #if os(macOS) 35 | case let .standardOverride(identifier, _): 36 | noPromptValet = .valet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) 37 | #endif 38 | case let .standard(identifier, _): 39 | noPromptValet = .valet(with: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) 40 | #if os(macOS) 41 | case let .sharedGroupOverride(identifier, _): 42 | noPromptValet = .sharedGroupValet(withExplicitlySet: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) 43 | #endif 44 | case let .sharedGroup(groupIdentifier, identifier, _): 45 | noPromptValet = .sharedGroupValet(with: groupIdentifier, identifier: identifier, accessibility: .whenPasscodeSetThisDeviceOnly) 46 | } 47 | 48 | return noPromptValet.canAccessKeychain() 49 | } 50 | 51 | /// - Parameters: 52 | /// - object: A Data value to be inserted into the keychain. 53 | /// - key: A key that can be used to retrieve the `object` from the keychain. 54 | /// - options: A base query used to scope the calls in the keychain. 55 | /// - Throws: An error of type `KeychainError`. 56 | static func setObject(_ object: Data, forKey key: String, options: [String : AnyHashable]) throws(KeychainError) { 57 | // Remove the key before trying to set it. This will prevent us from calling SecItemUpdate on an item stored on the Secure Enclave, which would cause iOS to prompt the user for authentication. 58 | try Keychain.removeObject(forKey: key, options: options) 59 | 60 | try Keychain.setObject(object, forKey: key, options: options) 61 | } 62 | 63 | #if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) 64 | /// - Parameters: 65 | /// - key: A key used to retrieve the desired object from the keychain. 66 | /// - userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. 67 | /// - context: The context to use for the query. 68 | /// - options: A base query used to scope the calls in the keychain. 69 | /// - Returns: The data currently stored in the keychain for the provided key. 70 | /// - Throws: An error of type `KeychainError`. 71 | static func object( 72 | forKey key: String, 73 | withPrompt userPrompt: String, 74 | context: LAContext?, 75 | options: [String : AnyHashable] 76 | ) throws(KeychainError) -> Data { 77 | var secItemQuery = options 78 | if !userPrompt.isEmpty { 79 | let context = context ?? LAContext() 80 | context.localizedReason = userPrompt 81 | secItemQuery[kSecUseAuthenticationContext as String] = context 82 | } 83 | return try Keychain.object(forKey: key, options: secItemQuery) 84 | } 85 | #else 86 | /// - Parameters: 87 | /// - key: A key used to retrieve the desired object from the keychain. 88 | /// - options: A base query used to scope the calls in the keychain. 89 | /// - Returns: The data currently stored in the keychain for the provided key. 90 | /// - Throws: An error of type `KeychainError`. 91 | static func object( 92 | forKey key: String, 93 | options: [String : AnyHashable] 94 | ) throws(KeychainError) -> Data { 95 | try Keychain.object(forKey: key, options: options) 96 | } 97 | #endif 98 | 99 | #if !os(tvOS) && canImport(LocalAuthentication) 100 | /// - Parameters: 101 | /// - key: The key to look up in the keychain. 102 | /// - options: A base query used to scope the calls in the keychain. 103 | /// - Returns: `true` if a value has been set for the given key, `false` otherwise. 104 | /// - Throws: An error of type `KeychainError`. 105 | static func containsObject(forKey key: String, options: [String : AnyHashable]) throws(KeychainError) -> Bool { 106 | var secItemQuery = options 107 | let context = LAContext() 108 | context.interactionNotAllowed = true 109 | secItemQuery[kSecUseAuthenticationContext as String] = context 110 | 111 | let status = Keychain.performCopy(forKey: key, options: secItemQuery) 112 | switch status { 113 | case errSecSuccess, 114 | errSecInteractionNotAllowed: 115 | // An item exists in the keychain if we could successfully copy the item, or if we got an error telling us we weren't allowed to copy the item since we couldn't prompt the user. 116 | return true 117 | case errSecItemNotFound: 118 | return false 119 | default: 120 | throw KeychainError(status: status) 121 | } 122 | } 123 | #endif 124 | 125 | /// - Parameters: 126 | /// - string: A String value to be inserted into the keychain. 127 | /// - key: A key that can be used to retrieve the `string` from the keychain. 128 | /// - options: A base query used to scope the calls in the keychain. 129 | /// - Throws: An error of type `KeychainError`. 130 | static func setString(_ string: String, forKey key: String, options: [String : AnyHashable]) throws(KeychainError) { 131 | // Remove the key before trying to set it. This will prevent us from calling SecItemUpdate on an item stored on the Secure Enclave, which would cause iOS to prompt the user for authentication. 132 | try Keychain.removeObject(forKey: key, options: options) 133 | 134 | try Keychain.setString(string, forKey: key, options: options) 135 | } 136 | 137 | #if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) 138 | /// - Parameters: 139 | /// - key: A key used to retrieve the desired object from the keychain. 140 | /// - userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. 141 | /// - context: The context to use for the query. 142 | /// - options: A base query used to scope the calls in the keychain. 143 | /// - Returns: The string currently stored in the keychain for the provided key. 144 | /// - Throws: An error of type `KeychainError`. 145 | static func string( 146 | forKey key: String, 147 | withPrompt userPrompt: String, 148 | context: LAContext?, 149 | options: [String : AnyHashable] 150 | ) throws(KeychainError) -> String { 151 | var secItemQuery = options 152 | if !userPrompt.isEmpty { 153 | let context = context ?? LAContext() 154 | context.localizedReason = userPrompt 155 | secItemQuery[kSecUseAuthenticationContext as String] = context 156 | } 157 | return try Keychain.string(forKey: key, options: secItemQuery) 158 | } 159 | #else 160 | /// - Parameters: 161 | /// - key: A key used to retrieve the desired object from the keychain. 162 | /// - options: A base query used to scope the calls in the keychain. 163 | /// - Returns: The string currently stored in the keychain for the provided key. 164 | /// - Throws: An error of type `KeychainError`. 165 | static func string( 166 | forKey key: String, 167 | options: [String : AnyHashable] 168 | ) throws(KeychainError) -> String { 169 | try Keychain.string(forKey: key, options: options) 170 | } 171 | 172 | #endif 173 | } 174 | -------------------------------------------------------------------------------- /Tests/ValetObjectiveCBridgeTests/VALSecureEnclaveValetTests.m: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/16/20. 2 | // Copyright © 2020 Square, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | //    http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | #import 17 | #import 18 | 19 | @interface VALSecureEnclaveValetTests : XCTestCase 20 | @end 21 | 22 | @implementation VALSecureEnclaveValetTests 23 | 24 | - (NSString *)identifier; 25 | { 26 | return @"identifier"; 27 | } 28 | 29 | - (NSString *)appIDPrefix; 30 | { 31 | return @"9XUJ7M53NG"; 32 | } 33 | 34 | - (NSString *)sharedAccessGroupIdentifier; 35 | { 36 | #if TARGET_OS_IPHONE 37 | return @"com.squareup.Valet-iOS-Test-Host-App"; 38 | #elif TARGET_OS_WATCH 39 | return @"com.squareup.ValetTouchIDTestApp.watchkitapp.watchkitextension"; 40 | #elif TARGET_OS_MAC 41 | return @"com.squareup.Valet-macOS-Test-Host-App"; 42 | #else 43 | // This will fail 44 | return @""; 45 | #endif 46 | } 47 | 48 | - (NSString *)groupPrefix; 49 | { 50 | #if TARGET_OS_IPHONE 51 | return @"group"; 52 | #elif TARGET_OS_WATCH 53 | return @"group"; 54 | #elif TARGET_OS_MAC 55 | return self.appIDPrefix; 56 | #else 57 | // This will fail 58 | return @""; 59 | #endif 60 | } 61 | 62 | - (NSString *)sharedAppGroupIdentifier; 63 | { 64 | return @"valet.test"; 65 | } 66 | 67 | - (void)test_valetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlDevicePasscode; 68 | { 69 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet valetWithIdentifier:self.identifier accessControl:VALSecureEnclaveAccessControlDevicePasscode]; 70 | XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlDevicePasscode); 71 | XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); 72 | } 73 | 74 | - (void)test_valetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlUserPresence; 75 | { 76 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet valetWithIdentifier:self.identifier accessControl:VALSecureEnclaveAccessControlUserPresence]; 77 | XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlUserPresence); 78 | XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); 79 | } 80 | 81 | - (void)test_valetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricAny; 82 | { 83 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet valetWithIdentifier:self.identifier accessControl:VALSecureEnclaveAccessControlBiometricAny]; 84 | XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricAny); 85 | XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); 86 | } 87 | 88 | - (void)test_valetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricCurrentSet; 89 | { 90 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet valetWithIdentifier:self.identifier accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; 91 | XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricCurrentSet); 92 | XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); 93 | } 94 | 95 | - (void)test_valetWithIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; 96 | { 97 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet valetWithIdentifier:@"" accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; 98 | XCTAssertNil(valet); 99 | } 100 | 101 | - (void)test_sharedAccessGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlDevicePasscode; 102 | { 103 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessControl:VALSecureEnclaveAccessControlDevicePasscode]; 104 | XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlDevicePasscode); 105 | XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); 106 | } 107 | 108 | - (void)test_sharedAccessGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlUserPresence; 109 | { 110 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessControl:VALSecureEnclaveAccessControlUserPresence]; 111 | XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlUserPresence); 112 | XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); 113 | } 114 | 115 | - (void)test_sharedAccessGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricAny; 116 | { 117 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessControl:VALSecureEnclaveAccessControlBiometricAny]; 118 | XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricAny); 119 | XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); 120 | } 121 | 122 | - (void)test_sharedAccessGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricCurrentSet; 123 | { 124 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:self.sharedAccessGroupIdentifier accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; 125 | XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricCurrentSet); 126 | XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); 127 | } 128 | 129 | - (void)test_sharedAccessGroupValetWithIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; 130 | { 131 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithAppIDPrefix:self.appIDPrefix sharedGroupIdentifier:@"" accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; 132 | XCTAssertNil(valet); 133 | } 134 | 135 | - (void)test_sharedAppGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlDevicePasscode; 136 | { 137 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessControl:VALSecureEnclaveAccessControlDevicePasscode]; 138 | XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlDevicePasscode); 139 | XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); 140 | } 141 | 142 | - (void)test_sharedAppGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlUserPresence; 143 | { 144 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessControl:VALSecureEnclaveAccessControlUserPresence]; 145 | XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlUserPresence); 146 | XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); 147 | } 148 | 149 | - (void)test_sharedAppGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricAny; 150 | { 151 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessControl:VALSecureEnclaveAccessControlBiometricAny]; 152 | XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricAny); 153 | XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); 154 | } 155 | 156 | - (void)test_sharedAppGroupValetWithIdentifier_accessControl_returnsCorrectValet_VALSecureEnclaveAccessControlBiometricCurrentSet; 157 | { 158 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:self.sharedAppGroupIdentifier accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; 159 | XCTAssertEqual(valet.accessControl, VALSecureEnclaveAccessControlBiometricCurrentSet); 160 | XCTAssertEqual([valet class], [VALSecureEnclaveValet class]); 161 | } 162 | 163 | - (void)test_sharedAppGroupValetWithIdentifier_accessibility_returnsNilWhenIdentifierIsEmpty; 164 | { 165 | VALSecureEnclaveValet *const valet = [VALSecureEnclaveValet sharedGroupValetWithGroupPrefix:self.groupPrefix sharedGroupIdentifier:@"" accessControl:VALSecureEnclaveAccessControlBiometricCurrentSet]; 166 | XCTAssertNil(valet); 167 | } 168 | 169 | @end 170 | --------------------------------------------------------------------------------