diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 9ea042d3d..25ce5081e 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -2,123 +2,25 @@ ## Reporting a Vulnerability -For reporting security-related vulnerabilities or exploits that [haven't been reported yet](https://github.com/cryptomator/ios/labels/type%3Asecurity-issue), contact us at: security@cryptomator.org +We take security seriously at Cryptomator. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. -
-PGP Key +To report a security vulnerability, please use the [GitHub Security Advisory feature](https://github.com/cryptomator/ios/security/advisories). This feature allows you to privately discuss, fix, and publish information about security vulnerabilities. -``` ------BEGIN PGP PUBLIC KEY BLOCK----- -Comment: GPGTools - https://gpgtools.org +If you prefer to report the vulnerability via email, please send an email to security@cryptomator.org. -mQINBFbgeicBEADM9AcU6DTgM5KZnBaJc6x9DBLr+TCMHntTt7YM9GLTlO2Z43Jt -oYoyqdRWAY28veqpLEFgRvvVD3fdBj/KUOxF1cr2JsErwXqbjwaLq0o/0KIXz7UK -a6pQSemZKfpOtJrfacofOTwvG6AuG9uakBYNMyxuojyOkoh3xsYS1KZ7TwPgCdET -t8/zva41Pa5kh5+GeSZJdCuygG6ynPBJEpmK5V7Qizvics5fziXecF+QaFZijafv -YahfxokvF9pXCQTmV4m57NQma9uK0w83U9nJCPjEd+x3wK0Hxrc1ojy8ZFTA1YND -AQg/MTABgHbQQkXDQhjS/TloOObqtbMBqNSbcSXpaR4teaCWKBl1MSq00nJLj8db -vPJGqfg7UbXhlALggp029/kskYlR5SmbxWquLbl0Xre3fDHuHEiWcJL6MS3454Wt -Mno13/4UhOlRFh5g0pLmPz7seOTJjDqc9abn/RXOLq0+3qX0gC0bDm5aCE5dQ2MV -FMbrrlw/dZESNLZvtB3gOsramSry1R3HVZ0QJ2vMaF2cxewebqcYbuecUNj6bxpv -5LEhEmqz6dG1meLLWDsvQLPEUWEIJnfpBiDSm342yxJq4pXnVF+aqAQsCL3FpmvZ -2j0FgFOs7iXOcFUJIiR0xUmWPk1NWYcUowqmRW8pMM9nFUzFF99iggPznwARAQAB -tC1DcnlwdG9tYXRvciBTdXBwb3J0IDxzdXBwb3J0QGNyeXB0b21hdG9yLm9yZz6J -AkAEEwEKACoCGwMFCQcrKAAFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AFAleu2cQC -GQEACgkQI7Xb75TU2B3+7A/7BKRWdo5/moCCEbBzYQ7vRMLFdwmjFFlSZ7aGC0fP -YHdeUwxPbO0cATwmNpGMma7rBn1FDg3Vto6/wottGxm+XIRwlyY84CD1VZAihZ/e -WvjOO28/7VgRy6PGKzlhpDSoT8GwFOgO69e7bEff1Zj562RZe7nXc4tDivILMB++ -KgmmSgtddygmNQCS3RD3KssGo+l+cSjsg09F5WAJ6nQe8Jq2hICq+o/P6UXPI5lX -bhvWYDn4/8sRHsIlGpQYYDDe0fz7IQKuSLAHpF5upNDxj6dYb05F8PPVrk6MW6nL -/kf1fZ27DlLN5/NFvhhBRuwxxoAFqPS7Iel3z7L0JkRUYmGLVB5m9Cqiw6FK8JRv -OtvakdDoKb5lVAoN5NeBfNBSqEcXVF/EdfTfIyyo7hZRA6xFMEVbmYbzt0sj0djV -ZOey2TOFrTCpkHfUUDgKvk5sn+F3u8mmPIbqquEzlFJSFjcyiYYDv22rg1In+zKV -Xmw4BFZRDS6IVSQRGlskRGJBixCaGyDYxHXXT2cg4Rk9uiCX11+0E9qlAsg6xPe6 -rnaYDT8dU0AFyVpDpshflXH3kVQSpiqZS3jkAk1/54ODO8pE80Zrnd5m5AMuNcmX -+9MkZKE+h0882UskDs1dyt26GU2hoy4lAeRUaut7zIK/WO6nnuLaTvGWT95RDz+q -kD2JAiIEEwEKAAwFAleu2iYFgweGH4AACgkQZnuGbqgkCgnmCA//U22uhyEC/Tp3 -Cbt5lctQmqbgMbjRBaHQyW52tPFMaq8vXMbo/5TTtVC6xsp2PJT84cxAd8KX8hWq -cPtF4wWCJGng/AzyxQ5dWfGvA/ll32ygjtJN3P/AvA9KlhG+6XYmS8cPkBkJBi6B -2yCdZT1cXc/TPAFzjgAwz7K9g3awG0OeOc/CXymH0DD/snkiwKQoucStolYywZGc -GszjMQgeT4zOc1wtEz24uL3dMNDlDcQMAh56YvK2oB0iMYmAFyX/IS+f2bM9paXi -HX+mg/z53iwgf5ZXbslNDbMTJ5GNksjEGjCFfDHAdNdgT+lcW4l2U7q4PYUaN4LA -DE9j2OlOlQ9qjucOgoCStirnTP7XHd4p31lgdz8+THOQowB5Ji95OkiNQAFCfxBt -mcA/bWnJZQDm7L8RVzHovBpAaK6vUjxEvR+DXdESSzyZwkpsZwGZcyqGRT26R1/L -JE5WvjKufNc5v3Cat320MjyrLZwVGRgvEpDMoCw3nTWl9AtOj5vgaakEWr7AnqET -xk7UFbYmdTlQqkWuLKubz9Rx/FbrBmvd6vwTHy1Dfl6QyMWNCClatgN00Hxped/6 -CErg+R/RXd8apGxnOuWDqoujPn5LOHzgJolp1Ox16nTiZe2G+LbDr3hqRFi1wW6w -ioMB4KpkdA03uyxJSWmDEMiR1l3Oxom0KUNyeXB0b21hdG9yIFByZXNzIDxwcmVz -c0BjcnlwdG9tYXRvci5vcmc+iQI9BBMBCgAnBQJXrtnDAhsDBQkHKygABQsJCAcD -BRUKCQgLBRYCAwEAAh4BAheAAAoJECO12++U1NgdQYMQAKCIzNJF8rURQcFLSv3J -sPBjRy2HCzCWm21MuhU+bsaZx7U9M9dgEjzLfxN9s19VsBH3WKLgok2FgiYSGka3 -6Oy/P8VFLFmHs7dS9i2fro2eF7i4zj/ZD/9t0jM4ZIgLpbzr5sTBld292nsfXGob -xOJeOx3oWYyR2FO9VQxXjC3JvJyZkFgoy0tauS4Mvii4cF56wJGcxDTbe1s7UaRC -a/fh4zgISZSBE3rYhCawkN4mqMDM5RDjrdtjKUPWk345HcjjQ4Wos8xw4YbGbNr9 -Pc7m2URYJJ0jFM4tnoRF6cmA3bT9tm8pcOFg+K/ycVrltVEy+A8Wj8UGjyP1uI1t -EqWHN3LZpIGfW0w9AGrw7OUI9czXcukfngj/DsOU3WMBDIM8pW9+zBpr75yIS6lz -C0IqksLXSqX0b/Rby4O+wb6UZ1ZFkaim2GGtAZV+nGXtdnEXSNFiP7ykzjZ02m/1 -7CKyj3VmdAgT56zEIypFSfxm9gOWsJPmfhSyuE8bFyoitgNxpheZk6xZy4upVMPR -WK3hutScU0yDv2HVCiA3o3Ggy42nmz9HpGF6W2DmBx4bhMaVs6I2VFyKdQzmJD/3 -FCWjwz8PiEgVGHGPnD+WdPFLhrc/44gF4h/VuLjkubtULGuTVvgjeTIJ5LR1Gmwc -YOk6eD7MAJPzJVj5/PYFtIbKiQIiBBMBCgAMBQJXrtonBYMHhh+AAAoJEGZ7hm6o -JAoJBh4P/1w88YMTKUHpFTfJEwH2hK36BZN96Bf/k+vP7n1Xxp3NheInJblHFOt/ -ccsup6am+APrk8gGtlIVmtVc3nO8WMsWxfJxGDecyRsNbessnODv/llyg3tzVU/H -tLk7gLiK0TcIsOLfeNXGTxRRSKWjVFsNfuixNCzzHa7tFq6ddVn9VRZ8fqJB2p21 -OogWSDqUo9q9Wfb4RkYHguDx+8Jzoo/MxR1TSt8gUO2xDvEbqgeQiMCLF8R0lO3Y -zz0FrpyOsFU1CxVp+wo55bWv1UdwgQKQt4o0m5/zDJ2RAtscXpd4YcTE+XxKeK+4 -qhihhkhLGpKsxzK5m9/qwMbodHwoBCBzfalkUR9xOq9yQIeEoC8XYL62NqB3BCSU -KfWFIHxUkE9WH5zHWaV+bhrlNgk7nz3xBfPf1P2mNIc1VUHoNqOZOmWwz2VaKLSW -f3GIqx9wGythFbLdXmUoC3W//DDYgQnvImvkncMqQ5nRHPf8uHcLQK5WZyIxpgWT -eKon5G/cj0BTptcBhapMwSIyfaC5FV7so0/CkOA6R9Fyq2VpGoHy7XPhFS+6ieLi -KUWhCvbuf2deWbSaJ0peMdzy1p72UXwrsEM0M3Fz+Jd8zvCaFzf5Fx27+pAAdlfg -4bT3/2gSf7S+cU3+DnYOH0NeRt2Z2mjEKg9OwttTO/oDboQHdZlrtDRDcnlwdG9t -YXRvciBTZWN1cml0eS1UZWFtIDxzZWN1cml0eUBjcnlwdG9tYXRvci5vcmc+iQI9 -BBMBCgAnBQJXrtnWAhsDBQkHKygABQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJ -ECO12++U1NgddzoQAI78+Nvm6VvNuptXJjEmrpHRyKCnHF9wH5kxvF8WZCgpOkJ4 -vONmyS+9ZlepnT83MpGm/MzdIMCnDJmDmqmA5ISBRcD7k9Gjzz5rPKwE2zDyo0M0 -wF1L2UEUqAlcvE0e4twZcP2DGoNqdSf6IaWsXhQMb1a/rTMsoGZLuTB8kCbv6Ntl -ULahcRToTB2shsbZjzE896P6X5hDCfGWl0Jhcbf53pnXX1dOsEw3et9AGru1IUMs -UGM+wpgTwagRj+XB/WY1x9IznKtiHTq83Fvt+3bkg0+NIcV3GDqXDIUtqIwy8gDd -4KgBU+LkyxXFDa4OxLc53n6b+Iy1nDosM+SiqSzdCCgEs/dY1tQBn/7P1GT18dEe -tFgeH/c6wLvEpDIc9urAsYXf8H+1uy7glWpWTq8DE0yhCr4adjCqlIsVHQQO4UUW -NfqMGEFpJ+3HjSSwnvDGY78lLQh5d4vqWV435aNaMqZg0gJIA0FtiP1fRtmT73BG -N/tBNiBxretFR4B+x/TWqPd5iJV7/MAn/pa1WSOcaxzJrVUsjXdgLQCqcHWd4/w1 -f4DU9cJjl3sxZlMdAlg8Q1bF+pmjQQ4WKZkqMtwpoUilfVXmL42ay1LBCgW68/uJ -OTyGfp8ntUsbbm5raGsny3TLqnacyG9hxcPGNTzD1+MrbUvfsc7+4U0dCZTuiQIi -BBMBCgAMBQJXrtonBYMHhh+AAAoJEGZ7hm6oJAoJ1DQP/R+1drZiZQU45ChMbfTb -XQjJRsUOGZp3PTWtx4KrVFvE8ea0PF+DZX5gLJYIU+iZmPXRpzFu6dKPbcZ7RfRt -5RRH102zDZzijt2CQd7YLO8wxUFoWX9X7DGgxXEcNjl9kFVmnyHgiTwTzuZ0Zy4y -PvoiwrhcZmXEYbOeV40gLFie6wuzz5IIcs01e30xIs+1/1gwmgI5UnG3jveUgmcj -f/lvg3POKiwrY5Uzw1FSruJx21X06wTpDcfOACID4L7aY9eg2B/qL2Xj8nuhejqG -+1AVTMk2o6pxkvevHmxYQfEpuWGCw0RCBn9ObWwz6Zn5J9pjGbMrM+b1/M2Ouv3N -cpoGgCSahKNsRMKO7RMrBG0jtLcasPSgZFYPJSZAAb+YhxKUbpPHzDIwTEjgM7CL -gKSyRTKyp5IoFK53bpXL/ZIjkAhMvyDult6+BL6vI0+h3BBA9I0FF2Qhe139xLv/ -DS7aDiYAE9vGMGoeCBfxJPwUsDU3hrGe/wgL7fR6nmN7R2QffisBHKHsklORy9t3 -w3YFRd5sBAxv+EOcdkgXEmqKOfVQ8KU9adQcxPDGMAK/esjVwxUxsaf2PF5noxxW -3zL2ureUO/mMoH5Cwr0BuM3HFb82t1JJd4IXlLEyNvDMFMwD2d7h37bGK7Y5hEsl -zL7Dm+wQRY8sxg4QOZHbJjQXuQINBFbgeicBEADnkxGSEL1zwACaiVqADKC6/pgO -MMWjxoENBT6r8Vnp1D5hfNDkEi9iXUpCEO6nzywBf3/4c4Yk1wBOBZ7YWyWXMf4v -2g1evxELO5z1UlAwna6HSl7G0omIBqzz1Er5IS7C9WEZM8ZggwcuswCrbxfz4+fN -t7cCL5QyOvuxez+vrn+VIgLQzKm+LV4Wc+OFbHIys+0saQUhItKO0/CsXGc8R314 -jdN5UsZk/MUdPPAs+6OCr8d3PpJvR6IST76TtN8aDjSS9T6em7dwdGFEwCGww3Jc -xrAkvvUmSlscz+rnvHA5DYQGK6NXLenB40sVQVfch1r1VqwvlzA0u7OovjwM8+7u -+DaBQ0YejbdnC7yfeE91LmZkG6jRKfvTJkv18tjNsgZsVmM13xzP67fCFIB9M+lN -t9zEldGKHVwm+06FHIWJsBDRgrquNb9xd1vgHHeIbJvKf+LqZhVrbKVEneG34Km+ -ndtb+mvcGc0fOoMU9lYrFaxAWl8oU9BchC9IyjcPZB445R+AhfTuoHSUViSCo6IO -TG0hQsJuNoKmDAU8l5sTsiFXuXBOo1wK8gTkRnhZHduZrZIjJXvT7efz1knLQ6eG -prZHf4CtbgHyAe2XZabetWtCsFcPbOjC7ezNK57UvVH98h2GkckxOM00BESMCTee -kYy7uG0v0rrajzHY1wARAQABiQIlBBgBCgAPBQJW4HonAhsMBQkHKygAAAoJECO1 -2++U1NgdyAsQAKZUVA6pY225BASkeNiW31L7K4VeRYpAdFkiRex2zQFtj9Vovfi1 -JeTs0fRm35dUsQraf1bkhsjEdPVZ3gD324/baauFO04KX+soyQvK/tUq8KO+5ALt -Ul5aAljuSwxfJWFpApv+Mbf7gOjm+77jirs7pgG/gCow/mkRlmKTwAmn2DXjkckC -2EH0mqmh5pdoNWKO7WeTFFbUmESsPcnB2FwTpEjHFvgHll+rmKpXZTgFYN4dDhhm -HsL/SCf/Nw+YIsuvErQ9TJVdJDLG8ZYatruk7dZZMPtFxvxM1Q36gDIpPEOKPkvm -dMXg6jHaIdYIaoMpzXFaXsQMdRuMtzbcA+CdwXVY55qGLtfmM/QuEiIJdDeeh7iB -+VAMyEFOOpi8IFhixaeMoZAmrKDqOkzPcMJVklLYq8N+b9p5JszYNwZEbpyWCACM -6K+iJzlWzW/OPZttGLJBgYuSYIJIuG80Cx5m5m1e5RAgQ1iT8nbfrS+gYttwP48J -V7SXQg7QugxG9l1vlK4VjnXiOFulJ7V0e/VyUBpJp3qHcCxFq3RnxVwlIqKZh+jm -Q1bk0H0Xodd27nQITfDP5ullByGW2Jrjs6SsXeR3jl9+t0XQfInU1L9d/wSOkMjL -9IMUt06lV4vB/WP2xioqLZiZ4eAi0E+lWkFxjZsgNs2xbOAYRThMB8a5 -=W1Ri ------END PGP PUBLIC KEY BLOCK----- -``` -
+PGP key fingerprint: `3647 9903 B23A E0A5 9359  9A3E 23B5 DBEF 94D4 D81D` ([public key](https://gist.github.com/cryptobot/864300b6b44ae2d2a15abedfe14bd040)) + +## Expectations + +When reporting a vulnerability, please provide us with a detailed report that includes: + +- A description of the vulnerability +- Steps to reproduce the vulnerability +- Possible impact of the vulnerability +- Any additional information that may be helpful + +We ask that you do not publicly disclose the vulnerability until we have had a chance to address it. + +## Thank You + +We appreciate your help in keeping Cryptomator for iOS secure. Thank you for your contributions to the security of our project. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2f7bb67ca..e2cfc5839 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,11 +6,14 @@ on: jobs: build: name: Build and test - runs-on: macos-12 + runs-on: macos-13 env: DERIVED_DATA_PATH: 'DerivedData' - DEVICE: 'iPhone 12 Pro' + DEVICE: 'iPhone 14 Pro' if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" + strategy: + matrix: + config: ['freemium', 'premium'] steps: - uses: actions/checkout@v3 - uses: actions/cache@v3 @@ -27,11 +30,22 @@ jobs: run: | cd fastlane ./scripts/create-cloud-access-secrets.sh + - name: Select Xcode 15.1 + run: sudo xcode-select -s /Applications/Xcode_15.1.app + - name: Configuration for freemium + if: ${{ matrix.config == 'freemium' }} + run: | + echo "BUILD_CMD=-enableCodeCoverage YES" >> $GITHUB_ENV + - name: Configuration for premium + if: ${{ matrix.config == 'premium' }} + run: | + echo "BUILD_CMD=SWIFT_ACTIVE_COMPILATION_CONDITIONS='\$(inherited) ALWAYS_PREMIUM'" >> $GITHUB_ENV - name: Build - run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild clean build-for-testing -scheme 'AllTests' -destination "name=$DEVICE" -derivedDataPath $DERIVED_DATA_PATH -enableCodeCoverage YES | xcpretty + run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild clean build-for-testing -scheme 'AllTests' -destination "name=$DEVICE" -derivedDataPath $DERIVED_DATA_PATH ${{ env.BUILD_CMD }} | xcpretty - name: Test run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild test-without-building -xctestrun $(find . -type f -name "*.xctestrun") -destination "name=$DEVICE" -derivedDataPath $DERIVED_DATA_PATH | xcpretty - name: Upload code coverage report + if: ${{ matrix.config == 'freemium' }} run: | gem install slather slather coverage -x --build-directory $DERIVED_DATA_PATH --ignore "$DERIVED_DATA_PATH/SourcePackages/*" --scheme AllTests Cryptomator.xcodeproj diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml new file mode 100644 index 000000000..1e5a848dd --- /dev/null +++ b/.github/workflows/no-response.yml @@ -0,0 +1,22 @@ +# Configuration for close-stale-issues - https://github.com/marketplace/actions/close-stale-issues + +name: 'Close awaiting response issues' +on: + schedule: + - cron: '00 09 * * *' + +jobs: + no-response: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v8 + with: + days-before-stale: 14 + days-before-close: 0 + days-before-pr-close: -1 + stale-issue-label: 'state:stale' + close-issue-message: "This issue has been automatically closed because there has been no response to our request for more information from the original author. With only the information that is currently in the issue, we don't have enough information to take action. Please reach out if you have or find the answers we need so that we can investigate further." + only-labels: 'state:awaiting-response' diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..f3a57687d --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,24 @@ +# Configuration for close-stale-issues - https://github.com/marketplace/actions/close-stale-issues + +name: 'Close stale issues' +on: + schedule: + - cron: '00 09 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v8 + with: + days-before-stale: 365 + days-before-close: 90 + exempt-issue-labels: 'type:security-issue,type:feature-request,type:enhancement,type:upstream-bug,state:awaiting-response,state:blocked,state:confirmed' + exempt-all-milestones: true + stale-issue-label: 'state:stale' + stale-pr-label: 'state:stale' + stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' + stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' diff --git a/.periphery.yml b/.periphery.yml new file mode 100644 index 000000000..51e8f9779 --- /dev/null +++ b/.periphery.yml @@ -0,0 +1,8 @@ +project: Cryptomator.xcodeproj +schemes: +- Cryptomator +targets: +- Cryptomator +- CryptomatorFileProvider +- FileProviderExtension +- FileProviderExtensionUI diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index b45695d55..849360299 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 4A03255525A3685500E63D7A /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A03255425A3685500E63D7A /* Coordinator.swift */; }; 4A03255E25A368BF00E63D7A /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A03255D25A368BF00E63D7A /* MainCoordinator.swift */; }; 4A03257825A36A6900E63D7A /* VaultListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A03257725A36A6900E63D7A /* VaultListViewController.swift */; }; 4A03258125A36B7D00E63D7A /* UIViewController+Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A03258025A36B7D00E63D7A /* UIViewController+Preview.swift */; }; @@ -19,6 +18,9 @@ 4A09BFC62684D599000E40AB /* VaultDetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A09BFC52684D599000E40AB /* VaultDetailItem.swift */; }; 4A09E54C27071F3C0056D32A /* ErrorMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A09E54B27071F3C0056D32A /* ErrorMapperTests.swift */; }; 4A09E54E27071F4F0056D32A /* ErrorMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A09E54D27071F4F0056D32A /* ErrorMapper.swift */; }; + 4A0AA12B2AB8DB1800CF24FD /* PermissionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0AA12A2AB8DB1800CF24FD /* PermissionProvider.swift */; }; + 4A0AA12D2ABA277800CF24FD /* PermissionProviderImplTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0AA12C2ABA277800CF24FD /* PermissionProviderImplTests.swift */; }; + 4A0AA12F2ABA2A1600CF24FD /* PermissionProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0AA12E2ABA2A1600CF24FD /* PermissionProviderMock.swift */; }; 4A0C07E225AC80C100B83211 /* UIView+Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0C07E125AC80C100B83211 /* UIView+Preview.swift */; }; 4A0C07EB25AC832900B83211 /* VaultListPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0C07EA25AC832900B83211 /* VaultListPosition.swift */; }; 4A0EAAD2296F604200E27B56 /* SessionTaskRegistratorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0EAAD1296F604200E27B56 /* SessionTaskRegistratorMock.swift */; }; @@ -254,7 +256,6 @@ 4AA782E2282A8FC0001A71E3 /* CachedFileManagerFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA782E1282A8FC0001A71E3 /* CachedFileManagerFactoryMock.swift */; }; 4AA782E4282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA782E3282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift */; }; 4AA782E6282A91BD001A71E3 /* CacheManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA782E5282A91BD001A71E3 /* CacheManagerMock.swift */; }; - 4AA8613725C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8613625C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift */; }; 4AA8614825C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8614725C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift */; }; 4AA8615125C1DB5E002A59F5 /* OpenExistingVaultPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8615025C1DB5E002A59F5 /* OpenExistingVaultPasswordViewController.swift */; }; 4AAD444727E26D1800D16707 /* UploadTaskManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AAD444627E26D1800D16707 /* UploadTaskManagerMock.swift */; }; @@ -326,8 +327,6 @@ 4AEBE8C22653FAD40031487F /* WorkflowMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEBE8C12653FAD40031487F /* WorkflowMiddleware.swift */; }; 4AED9A69286B303000352951 /* S3Authenticator+VC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED9A68286B303000352951 /* S3Authenticator+VC.swift */; }; 4AED9A6C286B305200352951 /* S3AuthenticationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED9A6B286B305200352951 /* S3AuthenticationView.swift */; }; - 4AED9A6F286B38DA00352951 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 4AED9A6E286B38DA00352951 /* Introspect */; }; - 4AED9A73286B3D6D00352951 /* SwiftUI+Focus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED9A72286B3D6C00352951 /* SwiftUI+Focus.swift */; }; 4AED9A77286B4BEE00352951 /* S3AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED9A76286B4BEE00352951 /* S3AuthenticationViewController.swift */; }; 4AED9A79286B4DF500352951 /* S3Authenticating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED9A78286B4DF500352951 /* S3Authenticating.swift */; }; 4AEE22F82861D6DC00A9C785 /* OpenVaultIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEE22F72861D6DC00A9C785 /* OpenVaultIntentHandler.swift */; }; @@ -353,6 +352,7 @@ 4AF45359271F38FC00CF1919 /* RenameVaultViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF45358271F38FC00CF1919 /* RenameVaultViewModelTests.swift */; }; 4AF4535D27205F6200CF1919 /* VaultDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF4535C27205F6200CF1919 /* VaultDetailCoordinator.swift */; }; 4AF4535F272066A600CF1919 /* RenameVaultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF4535E272066A600CF1919 /* RenameVaultViewController.swift */; }; + 4AF91A0F2AC2F025002357BA /* Dependencies in Frameworks */ = {isa = PBXBuildFile; productRef = 4AF91A0E2AC2F025002357BA /* Dependencies */; }; 4AF91CBE25A63FD600ACF01E /* VaultListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF91CBD25A63FD600ACF01E /* VaultListViewModel.swift */; }; 4AF91CC725A6437000ACF01E /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4AF91CC625A6437000ACF01E /* Colors.xcassets */; }; 4AF91CD025A71C5800ACF01E /* UIImage+CloudProviderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF91CCF25A71C5800ACF01E /* UIImage+CloudProviderType.swift */; }; @@ -361,6 +361,8 @@ 4AF91CEB25A7306E00ACF01E /* DatabaseManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF91CEA25A7306E00ACF01E /* DatabaseManagerTests.swift */; }; 4AF91CF425A8BB0D00ACF01E /* VaultListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF91CF325A8BB0D00ACF01E /* VaultListViewModelTests.swift */; }; 4AF91D0D25A8D5EF00ACF01E /* ListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF91D0C25A8D5EF00ACF01E /* ListViewModel.swift */; }; + 4AF9D44929C262B800EB3822 /* CryptomatorCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 4AF9D44829C262B800EB3822 /* CryptomatorCommon */; }; + 4AF9D44B29C293E600EB3822 /* HubAddVaultCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF9D44A29C293E600EB3822 /* HubAddVaultCoordinator.swift */; }; 4AFBFA142829206D00E30818 /* UploadProgressAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFBFA132829206D00E30818 /* UploadProgressAlertController.swift */; }; 4AFBFA1628293FE200E30818 /* UploadRetryingServiceSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFBFA1528293FE200E30818 /* UploadRetryingServiceSourceTests.swift */; }; 4AFBFA182829414A00E30818 /* ProgressManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFBFA172829414A00E30818 /* ProgressManagerMock.swift */; }; @@ -532,7 +534,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 4A03255425A3685500E63D7A /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; 4A03255D25A368BF00E63D7A /* MainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinator.swift; sourceTree = ""; }; 4A03257725A36A6900E63D7A /* VaultListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultListViewController.swift; sourceTree = ""; }; 4A03258025A36B7D00E63D7A /* UIViewController+Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Preview.swift"; sourceTree = ""; }; @@ -545,6 +546,9 @@ 4A09BFC52684D599000E40AB /* VaultDetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultDetailItem.swift; sourceTree = ""; }; 4A09E54B27071F3C0056D32A /* ErrorMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMapperTests.swift; sourceTree = ""; }; 4A09E54D27071F4F0056D32A /* ErrorMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMapper.swift; sourceTree = ""; }; + 4A0AA12A2AB8DB1800CF24FD /* PermissionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionProvider.swift; sourceTree = ""; }; + 4A0AA12C2ABA277800CF24FD /* PermissionProviderImplTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionProviderImplTests.swift; sourceTree = ""; }; + 4A0AA12E2ABA2A1600CF24FD /* PermissionProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionProviderMock.swift; sourceTree = ""; }; 4A0C07E125AC80C100B83211 /* UIView+Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Preview.swift"; sourceTree = ""; }; 4A0C07EA25AC832900B83211 /* VaultListPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultListPosition.swift; sourceTree = ""; }; 4A0EAAD1296F604200E27B56 /* SessionTaskRegistratorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTaskRegistratorMock.swift; sourceTree = ""; }; @@ -786,7 +790,6 @@ 4AA782E1282A8FC0001A71E3 /* CachedFileManagerFactoryMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedFileManagerFactoryMock.swift; sourceTree = ""; }; 4AA782E3282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFileProviderDomainProviderMock.swift; sourceTree = ""; }; 4AA782E5282A91BD001A71E3 /* CacheManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManagerMock.swift; sourceTree = ""; }; - 4AA8613625C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectedMasterkeyViewModel.swift; sourceTree = ""; }; 4AA8614725C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenExistingVaultChooseFolderViewController.swift; sourceTree = ""; }; 4AA8615025C1DB5E002A59F5 /* OpenExistingVaultPasswordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenExistingVaultPasswordViewController.swift; sourceTree = ""; }; 4AAD444627E26D1800D16707 /* UploadTaskManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadTaskManagerMock.swift; sourceTree = ""; }; @@ -865,7 +868,6 @@ 4AEBE8C12653FAD40031487F /* WorkflowMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkflowMiddleware.swift; sourceTree = ""; }; 4AED9A68286B303000352951 /* S3Authenticator+VC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "S3Authenticator+VC.swift"; sourceTree = ""; }; 4AED9A6B286B305200352951 /* S3AuthenticationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = S3AuthenticationView.swift; sourceTree = ""; }; - 4AED9A72286B3D6C00352951 /* SwiftUI+Focus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftUI+Focus.swift"; sourceTree = ""; }; 4AED9A76286B4BEE00352951 /* S3AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = S3AuthenticationViewController.swift; sourceTree = ""; }; 4AED9A78286B4DF500352951 /* S3Authenticating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = S3Authenticating.swift; sourceTree = ""; }; 4AEE22F72861D6DC00A9C785 /* OpenVaultIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenVaultIntentHandler.swift; sourceTree = ""; }; @@ -897,6 +899,7 @@ 4AF91CEA25A7306E00ACF01E /* DatabaseManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManagerTests.swift; sourceTree = ""; }; 4AF91CF325A8BB0D00ACF01E /* VaultListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultListViewModelTests.swift; sourceTree = ""; }; 4AF91D0C25A8D5EF00ACF01E /* ListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewModel.swift; sourceTree = ""; }; + 4AF9D44A29C293E600EB3822 /* HubAddVaultCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubAddVaultCoordinator.swift; sourceTree = ""; }; 4AFBFA132829206D00E30818 /* UploadProgressAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadProgressAlertController.swift; sourceTree = ""; }; 4AFBFA1528293FE200E30818 /* UploadRetryingServiceSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadRetryingServiceSourceTests.swift; sourceTree = ""; }; 4AFBFA172829414A00E30818 /* ProgressManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressManagerMock.swift; sourceTree = ""; }; @@ -999,6 +1002,12 @@ 7469AD99266E26B0000DCD45 /* URL+Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Zip.swift"; sourceTree = ""; }; 747C35162762A3F500E4CA28 /* AttributedTextHeaderFooterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedTextHeaderFooterViewModel.swift; sourceTree = ""; }; 74833F9D27E4CCD800C1C5F0 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = ""; }; + 748BF2062B571AE7006304AD /* ba */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ba; path = ba.lproj/Localizable.strings; sourceTree = ""; }; + 748BF20D2B571B0A006304AD /* ba */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ba; path = ba.lproj/Intents.strings; sourceTree = ""; }; + 748BF20E2B571BAA006304AD /* fil */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fil; path = fil.lproj/Localizable.strings; sourceTree = ""; }; + 748BF20F2B571BB4006304AD /* fil */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fil; path = fil.lproj/Intents.strings; sourceTree = ""; }; + 748BF2102B571C0C006304AD /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + 748BF2112B571C11006304AD /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Intents.strings; sourceTree = ""; }; 74A1B13D2726A9E60098224B /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = ""; }; 74AE94EF27A0282300D71AEC /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; 74AE94F027A0283500D71AEC /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = ""; }; @@ -1060,6 +1069,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4AF9D44929C262B800EB3822 /* CryptomatorCommon in Frameworks */, 4A9BED67268F379300721BAA /* libCryptomatorFileProvider.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1078,7 +1088,6 @@ buildActionMask = 2147483647; files = ( 4A9172822619F17C003C4043 /* CryptomatorCommon in Frameworks */, - 4AED9A6F286B38DA00352951 /* Introspect in Frameworks */, 4A1521E427C55EA2006C96B2 /* TPInAppReceipt in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1095,6 +1104,7 @@ buildActionMask = 2147483647; files = ( 4A91728B2619F1D0003C4043 /* CryptomatorCommonCore in Frameworks */, + 4AF91A0F2AC2F025002357BA /* Dependencies in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1194,6 +1204,7 @@ 4A9C8DFC27A007C2000063E4 /* FileProviderNotificatorTests.swift */, 4AFBFA19282946BF00E30818 /* InMemoryProgressManagerTests.swift */, 4AB1D4EF27D20420009060AB /* LocalURLProviderTests.swift */, + 4A0AA12C2ABA277800CF24FD /* PermissionProviderImplTests.swift */, 4AC1157727F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift */, 4ADC66C427A7F6D6002E6CC7 /* UnlockMonitorTests.swift */, 4A4F47F224B875070033328B /* URL+NameCollisionExtensionTests.swift */, @@ -1441,6 +1452,7 @@ 4A3D655E268099F9000DA764 /* VaultCoordinatorError.swift */, 4A2FD08125B5E2BA008565C8 /* VaultInstalling.swift */, 4A644B45267A3D21008CBB9A /* CreateNewVault */, + 4AF9D44C29C293F800EB3822 /* Hub */, 4A1EB0D6268A6CF5006D072B /* LocalVault */, 4AA8613F25C1AC4D002A59F5 /* OpenExistingVault */, ); @@ -1489,7 +1501,6 @@ 4A644B56267C958F008CBB9A /* ChildCoordinator.swift */, 4AFCE53925B9D6A60069C4FC /* CloudAuthenticator.swift */, 4AFCE51E25B89CD80069C4FC /* CloudProviderType+Localization.swift */, - 4A03255425A3685500E63D7A /* Coordinator.swift */, 4AF91CE125A7234500ACF01E /* DatabaseManager.swift */, 4A8A6423286CA72B001F5EB9 /* DefaultShowEditAccountBehavior.swift */, 4A512D69274277FF00DC26F8 /* EditableDataSource.swift */, @@ -1502,7 +1513,6 @@ 4A4246F727565D87005BE82D /* PoppingCloseCoordinator.swift */, 4A447E0325BF0B0F00D9520D /* SingleSectionTableViewController.swift */, 4A61F6B8274582E3007AA422 /* StaticUITableViewController.swift */, - 4AED9A72286B3D6C00352951 /* SwiftUI+Focus.swift */, 4A61F6B62745353E007AA422 /* TableViewModel.swift */, 4AF91CCF25A71C5800ACF01E /* UIImage+CloudProviderType.swift */, 4AC8626F273598CC00E15BA5 /* UIViewController+ProgressHUDError.swift */, @@ -1667,7 +1677,6 @@ 4AA8613F25C1AC4D002A59F5 /* OpenExistingVault */ = { isa = PBXGroup; children = ( - 4AA8613625C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift */, 4A753DB82678A226005F79C1 /* OpenExistingLegacyVaultPasswordViewModel.swift */, 4AA8614725C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift */, 4A2FD08A25B5E437008565C8 /* OpenExistingVaultCoordinator.swift */, @@ -1721,6 +1730,7 @@ 4AEECD3E279EC48200C6E2B5 /* NSFileProviderChangeObserverMock.swift */, 4AA782E3282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift */, 4AEECD3A279EB24300C6E2B5 /* NSFileProviderEnumerationObserverMock.swift */, + 4A0AA12E2ABA2A1600CF24FD /* PermissionProviderMock.swift */, 4AFBFA172829414A00E30818 /* ProgressManagerMock.swift */, 4A0EAAD1296F604200E27B56 /* SessionTaskRegistratorMock.swift */, 4ADC66C627A95E67002E6CC7 /* UnlockMonitorTaskExecutorMock.swift */, @@ -1874,6 +1884,14 @@ path = DB; sourceTree = ""; }; + 4AF9D44C29C293F800EB3822 /* Hub */ = { + isa = PBXGroup; + children = ( + 4AF9D44A29C293E600EB3822 /* HubAddVaultCoordinator.swift */, + ); + path = Hub; + sourceTree = ""; + }; 740375D82587AE7B0023FF53 /* CryptomatorFileProvider */ = { isa = PBXGroup; children = ( @@ -1897,6 +1915,7 @@ 4AB1D4EB27D0E027009060AB /* LocalURLProviderType.swift */, 4AA782DD282A8250001A71E3 /* NSFileProviderDomainProvider.swift */, 4AEE6EE02822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift */, + 4A0AA12A2AB8DB1800CF24FD /* PermissionProvider.swift */, 4AEE6EE92825716400E1B35E /* ProgressManager.swift */, 4AC1157527F5BD890023F51B /* Promise+AllIgnoringResult.swift */, 4ADD233F26737CD400374E4E /* RootFileProviderItem.swift */, @@ -2129,9 +2148,13 @@ buildRules = ( ); dependencies = ( + 4AC4C98E288AD858008C6D2B /* PBXTargetDependency */, 4A9BED69268F379300721BAA /* PBXTargetDependency */, ); name = FileProviderExtensionUI; + packageProductDependencies = ( + 4AF9D44829C262B800EB3822 /* CryptomatorCommon */, + ); productName = "File Provider ExtensionUI"; productReference = 4AA621E4249A6A8400A0BCBD /* FileProviderExtensionUI.appex */; productType = "com.apple.product-type.app-extension"; @@ -2180,7 +2203,6 @@ packageProductDependencies = ( 4A9172812619F17C003C4043 /* CryptomatorCommon */, 4A1521E327C55EA2006C96B2 /* TPInAppReceipt */, - 4AED9A6E286B38DA00352951 /* Introspect */, ); productName = Cryptomator; productReference = 4AE97DA824572E4900452814 /* Cryptomator.app */; @@ -2220,6 +2242,7 @@ name = CryptomatorFileProvider; packageProductDependencies = ( 4A91728A2619F1D0003C4043 /* CryptomatorCommonCore */, + 4AF91A0E2AC2F025002357BA /* Dependencies */, ); productName = CryptomatorFileProvider; productReference = 740375D72587AE7A0023FF53 /* libCryptomatorFileProvider.a */; @@ -2278,16 +2301,19 @@ Base, de, ar, + ba, bn, ca, cs, da, el, es, + fil, fr, he, hi, hr, + hu, id, it, ja, @@ -2310,7 +2336,6 @@ mainGroup = 4A5E5B202453119100BD6298; packageReferences = ( 4A1521E227C55EA2006C96B2 /* XCRemoteSwiftPackageReference "TPInAppReceipt" */, - 4AED9A6D286B38D900352951 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, ); productRefGroup = 4A5E5B2A2453119100BD6298 /* Products */; projectDirPath = ""; @@ -2441,7 +2466,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ -f ./fastlane/scripts/.cloud-access-secrets.sh ]; then\n source ./fastlane/scripts/.cloud-access-secrets.sh \"${CONFIG_NAME}\"\nelse\n echo \"warning: .cloud-access-secrets.sh could not be found, please see README for instructions\"\nfi\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:0 string db-${DROPBOX_APP_KEY}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:1 string ${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:2 string ${ONEDRIVE_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\necho \"Updated ${TARGET_BUILD_DIR}/${INFOPLIST_PATH} by adding URL schemes\"\n"; + shellScript = "if [ -f ./fastlane/scripts/.cloud-access-secrets.sh ]; then\n source ./fastlane/scripts/.cloud-access-secrets.sh \"${CONFIG_NAME}\"\nelse\n echo \"warning: .cloud-access-secrets.sh could not be found, please see README for instructions\"\nfi\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:0 string db-${DROPBOX_APP_KEY}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:1 string ${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:2 string ${ONEDRIVE_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:3 string ${HUB_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\necho \"Updated ${TARGET_BUILD_DIR}/${INFOPLIST_PATH} by adding URL schemes\"\n"; }; 742595D72552EE0000A8A008 /* Set Build Number */ = { isa = PBXShellScriptBuildPhase; @@ -2530,6 +2555,7 @@ 4AFBFA1628293FE200E30818 /* UploadRetryingServiceSourceTests.swift in Sources */, 4AB1C33C265E9DBC00DC7A49 /* CloudTaskExecutorTestCase.swift in Sources */, 4AE5196727F495BF00BA6E4A /* WorkflowDependencyTasksCollectionMock.swift in Sources */, + 4A0AA12F2ABA2A1600CF24FD /* PermissionProviderMock.swift in Sources */, 4AC1157827F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift in Sources */, 4AE5196527F48D6600BA6E4A /* WorkflowDependencyFactoryTests.swift in Sources */, 4A49FABE271ECDE80069A0CC /* ItemEnumerationTaskManagerTests.swift in Sources */, @@ -2563,6 +2589,7 @@ 4ADC66C527A7F6D6002E6CC7 /* UnlockMonitorTests.swift in Sources */, 4ABC08D7250D1EB600E3CEDC /* DeletionTaskManagerTests.swift in Sources */, 4A511D45265EB13B000A0E01 /* ItemEnumerationTaskTests.swift in Sources */, + 4A0AA12D2ABA277800CF24FD /* PermissionProviderImplTests.swift in Sources */, 4A2F373724B47DB800460FD3 /* UploadTaskManagerTests.swift in Sources */, 4A248221266B8D37002D9F59 /* FileProviderAdapterImportDocumentTests.swift in Sources */, 4A511D5326615439000A0E01 /* ReparentTaskExecutorTests.swift in Sources */, @@ -2703,6 +2730,7 @@ 4A4246F827565D87005BE82D /* PoppingCloseCoordinator.swift in Sources */, 4A66F58B25C489C7001BE15E /* OpenExistingVaultPasswordViewModel.swift in Sources */, 4A03258125A36B7D00E63D7A /* UIViewController+Preview.swift in Sources */, + 4AF9D44B29C293E600EB3822 /* HubAddVaultCoordinator.swift in Sources */, 4A21B49226BBFFE9000D13DF /* AttributedTextHeaderFooterView.swift in Sources */, 4A707802278DC32800AEF4CE /* VaultKeepUnlockedViewModel.swift in Sources */, 4A90E7C327C79DCF00BC858B /* PurchaseCell.swift in Sources */, @@ -2762,7 +2790,6 @@ 4A5AC43D275A306F00342AA7 /* TrialExpiredNavigationController.swift in Sources */, 4A53CC13267CC1C100853BB3 /* CreateNewVaultPasswordViewController.swift in Sources */, 4A6A51FF268B1BEB006F7368 /* OpenExistingLocalVaultCoordinator.swift in Sources */, - 4A03255525A3685500E63D7A /* Coordinator.swift in Sources */, 4AED9A77286B4BEE00352951 /* S3AuthenticationViewController.swift in Sources */, 4A644B47267A3D43008CBB9A /* SetVaultNameViewModel.swift in Sources */, 4A1EB0D02689C7F8006D072B /* DetectedVaultFailureView.swift in Sources */, @@ -2772,7 +2799,6 @@ 4A587FA828B55CD600C69A1E /* WebDAVCredentialCoordinator.swift in Sources */, 4A53CC11267CBFA100853BB3 /* AddVaultSuccessCoordinator.swift in Sources */, 4A6CF80027428CCB0061380A /* VaultCellViewModel.swift in Sources */, - 4AED9A73286B3D6D00352951 /* SwiftUI+Focus.swift in Sources */, 4A8D05D625C5CBE10082C5F7 /* AddVaultSuccessViewController.swift in Sources */, 4A0C07EB25AC832900B83211 /* VaultListPosition.swift in Sources */, 4A3D658226838991000DA764 /* OpenExistingLocalVaultViewModel.swift in Sources */, @@ -2811,7 +2837,6 @@ 4A447E5625BF1F6A00D9520D /* CloudItemCell.swift in Sources */, 4A5F48EE272AA02A0084135F /* MaintenanceModeError+Localization.swift in Sources */, 4A63E4672742A8CB00026989 /* ListViewController.swift in Sources */, - 4AA8613725C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift in Sources */, 7460FFEF26FCC6FC0018BCC4 /* OnboardingNavigationController.swift in Sources */, 4A1EB0D8268A6DE1006D072B /* AddLocalVaultViewController.swift in Sources */, 4A7B97E525B6F86E0044B7FB /* AccountListPosition.swift in Sources */, @@ -2930,6 +2955,7 @@ 4A511D5D26668E47000A0E01 /* ReparentTaskRecord.swift in Sources */, 747F2F272587BC250072FB30 /* ReparentTask.swift in Sources */, 747F2F282587BC250072FB30 /* ReparentTaskDBManager.swift in Sources */, + 4A0AA12B2AB8DB1800CF24FD /* PermissionProvider.swift in Sources */, 4AB1D4EC27D0E027009060AB /* LocalURLProviderType.swift in Sources */, 4A511D4E2660FF9E000A0E01 /* WorkflowScheduler.swift in Sources */, 4AD9481A2909A66900072110 /* MaintenanceModeHelperServiceSource.swift in Sources */, @@ -2985,6 +3011,10 @@ target = 740375D62587AE7A0023FF53 /* CryptomatorFileProvider */; targetProxy = 4A9BED68268F379300721BAA /* PBXContainerItemProxy */; }; + 4AC4C98E288AD858008C6D2B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = 4AC4C98D288AD858008C6D2B /* AppAuth */; + }; 4AD3D7DC282EBDE7008188CD /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 4AD3D7D3282EBDE7008188CD /* CryptomatorIntents */; @@ -3025,16 +3055,19 @@ 4A2745F7284769B800E70D5F /* en */, 74275ACC28478DFA0058AD25 /* de */, 74275ACD28478DFC0058AD25 /* ar */, + 748BF20D2B571B0A006304AD /* ba */, 74275ACE28478DFD0058AD25 /* bn */, 74275ACF28478DFE0058AD25 /* ca */, 74275AD428478E020058AD25 /* cs */, 741CD1CF2939083C00577FDE /* da */, 74275AD728478E050058AD25 /* el */, 74275AE428478E130058AD25 /* es */, + 748BF20F2B571BB4006304AD /* fil */, 74275AD628478E040058AD25 /* fr */, 74B46E5629BB863C000C1CC0 /* he */, 74275AD828478E060058AD25 /* hi */, 74275AD328478E010058AD25 /* hr */, + 748BF2112B571C11006304AD /* hu */, 74275AD928478E070058AD25 /* id */, 74275ADA28478E080058AD25 /* it */, 74275ADB28478E090058AD25 /* ja */, @@ -3071,16 +3104,19 @@ 742679FA26A56B33004C61BC /* en */, 742679FE26A578E2004C61BC /* de */, 74AE94EF27A0282300D71AEC /* ar */, + 748BF2062B571AE7006304AD /* ba */, 74E93B742810109E0047A116 /* bn */, 74AE94F027A0283500D71AEC /* ca */, 74BDA62B26CE8AE1007FBD72 /* cs */, 741CD1C82939080D00577FDE /* da */, 74267A0326A5793E004C61BC /* el */, 74267A0426A57944004C61BC /* es */, + 748BF20E2B571BAA006304AD /* fil */, 74267A0526A57947004C61BC /* fr */, 74B46E5529BB8629000C1CC0 /* he */, 74A1B13D2726A9E60098224B /* hi */, 74E93B75281010E50047A116 /* hr */, + 748BF2102B571C0C006304AD /* hu */, 74AE94F127A0285400D71AEC /* id */, 74267A0A26A5795C004C61BC /* it */, 74267A0B26A57960004C61BC /* ja */, @@ -3263,8 +3299,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.4.9; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 2.5.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -3325,8 +3361,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.4.9; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 2.5.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=200 -Xfrontend -warn-long-function-bodies=200"; @@ -3672,14 +3708,6 @@ minimumVersion = 3.3.0; }; }; - 4AED9A6D286B38D900352951 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/siteline/SwiftUI-Introspect.git"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.1.4; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -3700,10 +3728,17 @@ isa = XCSwiftPackageProductDependency; productName = CryptomatorCommonCore; }; - 4AED9A6E286B38DA00352951 /* Introspect */ = { + 4AC4C98D288AD858008C6D2B /* AppAuth */ = { isa = XCSwiftPackageProductDependency; - package = 4AED9A6D286B38D900352951 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; - productName = Introspect; + productName = AppAuth; + }; + 4AF91A0E2AC2F025002357BA /* Dependencies */ = { + isa = XCSwiftPackageProductDependency; + productName = Dependencies; + }; + 4AF9D44829C262B800EB3822 /* CryptomatorCommon */ = { + isa = XCSwiftPackageProductDependency; + productName = CryptomatorCommon; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6620f8acf..b87dbea32 100644 --- a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,196 +1,248 @@ { - "object": { - "pins": [ - { - "package": "AppAuth", - "repositoryURL": "https://github.com/openid/AppAuth-iOS.git", - "state": { - "branch": null, - "revision": "3d36a58a2b736f7bc499453e996a704929b25080", - "version": "1.6.0" - } - }, - { - "package": "ASN1Swift", - "repositoryURL": "https://github.com/tikhop/ASN1Swift", - "state": { - "branch": null, - "revision": "b53bee03a942623db25afc5bfb80227b2cb3b425", - "version": "1.2.4" - } - }, - { - "package": "AWSiOSSDKV2", - "repositoryURL": "https://github.com/aws-amplify/aws-sdk-ios-spm.git", - "state": { - "branch": null, - "revision": "51d99d74be7249ac6444581bd1e394fb60ea86a3", - "version": "2.30.4" - } - }, - { - "package": "Base32", - "repositoryURL": "https://github.com/norio-nomura/Base32.git", - "state": { - "branch": null, - "revision": "c4bc0a49689999ae2c7c778f3830a6a6e694efb8", - "version": "0.9.0" - } - }, - { - "package": "CryptomatorCloudAccess", - "repositoryURL": "https://github.com/cryptomator/cloud-access-swift.git", - "state": { - "branch": null, - "revision": "c9eaa84a3e73aceef10fc724d386eab3d6e3cbb7", - "version": "1.7.5" - } - }, - { - "package": "CocoaLumberjack", - "repositoryURL": "https://github.com/CocoaLumberjack/CocoaLumberjack.git", - "state": { - "branch": null, - "revision": "0188d31089b5881a269e01777be74c7316924346", - "version": "3.8.0" - } - }, - { - "package": "CryptomatorCryptoLib", - "repositoryURL": "https://github.com/cryptomator/cryptolib-swift.git", - "state": { - "branch": null, - "revision": "6e5dbea6e05742ad82a074bf7ee8c3305d92fbae", - "version": "1.1.0" - } - }, - { - "package": "ObjectiveDropboxOfficial", - "repositoryURL": "https://github.com/phil1995/dropbox-sdk-obj-c-spm.git", - "state": { - "branch": null, - "revision": "f0eafe25d26c52377c4a1c08f1dbd77320164994", - "version": "7.0.0" - } - }, - { - "package": "GoogleAPIClientForREST", - "repositoryURL": "https://github.com/google/google-api-objectivec-client-for-rest.git", - "state": { - "branch": null, - "revision": "260501c0425e95e038c65436436161266bf548e9", - "version": "3.0.0" - } - }, - { - "package": "GRDB", - "repositoryURL": "https://github.com/groue/GRDB.swift.git", - "state": { - "branch": null, - "revision": "dd7e7f39e8e4d7a22d258d9809a882f914690b01", - "version": "5.26.1" - } - }, - { - "package": "GTMSessionFetcher", - "repositoryURL": "https://github.com/google/gtm-session-fetcher.git", - "state": { - "branch": null, - "revision": "efda500b6d9858d38a76dbfbfa396bd644692e4a", - "version": "3.0.0" - } - }, - { - "package": "GTMAppAuth", - "repositoryURL": "https://github.com/google/GTMAppAuth.git", - "state": { - "branch": null, - "revision": "cee3c709307912d040bd1e06ca919875a92339c6", - "version": "2.0.0" - } - }, - { - "package": "JOSESwift", - "repositoryURL": "https://github.com/airsidemobile/JOSESwift.git", - "state": { - "branch": null, - "revision": "10ed3b6736def7c26eb87135466b1cb46ea7e37f", - "version": "2.4.0" - } - }, - { - "package": "MSAL", - "repositoryURL": "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git", - "state": { - "branch": null, - "revision": "31a806298d6aa71b40504e7ebda6d6a8923f0ebf", - "version": "1.2.5" - } - }, - { - "package": "MSGraphClientModels", - "repositoryURL": "https://github.com/phil1995/msgraph-sdk-objc-models-spm.git", - "state": { - "branch": null, - "revision": "172b07fe8a7da6072149e2fd92051a510b25035e", - "version": "1.3.0" - } - }, - { - "package": "MSGraphClientSDK", - "repositoryURL": "https://github.com/phil1995/msgraph-sdk-objc-spm.git", - "state": { - "branch": null, - "revision": "0320c6a99207b53288970382afcf5054852f9724", - "version": "1.0.0" - } - }, - { - "package": "PCloudSDKSwift", - "repositoryURL": "https://github.com/pCloud/pcloud-sdk-swift.git", - "state": { - "branch": null, - "revision": "6da4ca6bb4e7068145d9325988e29862d26300ba", - "version": "3.2.0" - } - }, - { - "package": "Promises", - "repositoryURL": "https://github.com/google/promises.git", - "state": { - "branch": null, - "revision": "611337c330350c9c1823ad6d671e7f936af5ee13", - "version": "2.0.0" - } - }, - { - "package": "swift-log", - "repositoryURL": "https://github.com/apple/swift-log.git", - "state": { - "branch": null, - "revision": "6fe203dc33195667ce1759bf0182975e4653ba1c", - "version": "1.4.4" - } - }, - { - "package": "Introspect", - "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git", - "state": { - "branch": null, - "revision": "f2616860a41f9d9932da412a8978fec79c06fe24", - "version": "0.1.4" - } - }, - { - "package": "TPInAppReceipt", - "repositoryURL": "https://github.com/tikhop/TPInAppReceipt.git", - "state": { - "branch": null, - "revision": "5b830d6ce6c34bb4bb976917576ab560e7945037", - "version": "3.3.4" - } - } - ] - }, - "version": 1 + "pins" : [ + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS.git", + "state" : { + "revision" : "71cde449f13d453227e687458144bde372d30fc7", + "version" : "1.6.2" + } + }, + { + "identity" : "asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/leif-ibsen/ASN1", + "state" : { + "revision" : "5bb6eca2e4b250995f189c3d04ec53b6cd8257c5", + "version" : "2.2.0" + } + }, + { + "identity" : "asn1swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tikhop/ASN1Swift", + "state" : { + "revision" : "b53bee03a942623db25afc5bfb80227b2cb3b425", + "version" : "1.2.4" + } + }, + { + "identity" : "aws-sdk-ios-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aws-amplify/aws-sdk-ios-spm.git", + "state" : { + "revision" : "59fdc9ca7ff3f5d38e07af27526a527c199b8de6", + "version" : "2.33.7" + } + }, + { + "identity" : "base32", + "kind" : "remoteSourceControl", + "location" : "https://github.com/norio-nomura/Base32.git", + "state" : { + "revision" : "c4bc0a49689999ae2c7c778f3830a6a6e694efb8", + "version" : "0.9.0" + } + }, + { + "identity" : "bigint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/leif-ibsen/BigInt", + "state" : { + "revision" : "3fe07ec38afa732e86d4f3e867cb43b05d004941", + "version" : "1.14.0" + } + }, + { + "identity" : "cloud-access-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/cryptomator/cloud-access-swift.git", + "state" : { + "revision" : "63fd1cfee9e4d1c0a8d585dd0c7008eb37d2f037", + "version" : "1.9.0" + } + }, + { + "identity" : "cocoalumberjack", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", + "state" : { + "revision" : "67ec5818a757aba4d7c534e21a905d878d128dbf", + "version" : "3.8.1" + } + }, + { + "identity" : "cryptolib-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/cryptomator/cryptolib-swift.git", + "state" : { + "revision" : "6e5dbea6e05742ad82a074bf7ee8c3305d92fbae", + "version" : "1.1.0" + } + }, + { + "identity" : "digest", + "kind" : "remoteSourceControl", + "location" : "https://github.com/leif-ibsen/Digest", + "state" : { + "revision" : "fd501645c5f14c17207c4ada4281a1e6b7cb03df", + "version" : "1.1.0" + } + }, + { + "identity" : "dropbox-sdk-obj-c-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/phil1995/dropbox-sdk-obj-c-spm.git", + "state" : { + "revision" : "f0eafe25d26c52377c4a1c08f1dbd77320164994", + "version" : "7.0.0" + } + }, + { + "identity" : "google-api-objectivec-client-for-rest", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/google-api-objectivec-client-for-rest.git", + "state" : { + "revision" : "40930b2c3add6234b8be1a780c08cf88b6a7a1f7", + "version" : "3.2.0" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/GRDB.swift.git", + "state" : { + "revision" : "dd7e7f39e8e4d7a22d258d9809a882f914690b01", + "version" : "5.26.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "d415594121c9e8a4f9d79cecee0965cf35e74dbd", + "version" : "3.1.1" + } + }, + { + "identity" : "gtmappauth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GTMAppAuth.git", + "state" : { + "revision" : "41aba100f28395ebe842cd66e5d371cdd46c6792", + "version" : "4.0.0" + } + }, + { + "identity" : "joseswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tobihagemann/JOSESwift.git", + "state" : { + "revision" : "3544f8117908ef12ea13b1c0927e0e3c0d30ee01", + "version" : "2.4.1-cryptomator" + } + }, + { + "identity" : "microsoft-authentication-library-for-objc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git", + "state" : { + "revision" : "e9ef281b2f281c3ba2d32608138b1431cba5e4df", + "version" : "1.2.20" + } + }, + { + "identity" : "msgraph-sdk-objc-models-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/phil1995/msgraph-sdk-objc-models-spm.git", + "state" : { + "revision" : "172b07fe8a7da6072149e2fd92051a510b25035e", + "version" : "1.3.0" + } + }, + { + "identity" : "msgraph-sdk-objc-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/phil1995/msgraph-sdk-objc-spm.git", + "state" : { + "revision" : "0320c6a99207b53288970382afcf5054852f9724", + "version" : "1.0.0" + } + }, + { + "identity" : "pcloud-sdk-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pCloud/pcloud-sdk-swift.git", + "state" : { + "revision" : "6da4ca6bb4e7068145d9325988e29862d26300ba", + "version" : "3.2.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", + "version" : "2.3.1" + } + }, + { + "identity" : "simple-swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PhilLibs/simple-swift-dependencies", + "state" : { + "revision" : "36e2e7732b5fe2bfec76e4af78d2ef532fe09456", + "version" : "0.1.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", + "version" : "1.5.3" + } + }, + { + "identity" : "swiftecc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/leif-ibsen/SwiftECC", + "state" : { + "revision" : "18c0e462882d0a4fa910472a0a6cc13ef97bbc21", + "version" : "5.0.0" + } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", + "state" : { + "revision" : "121c146fe591b1320238d054ae35c81ffa45f45a", + "version" : "0.12.0" + } + }, + { + "identity" : "tpinappreceipt", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tikhop/TPInAppReceipt.git", + "state" : { + "revision" : "5b830d6ce6c34bb4bb976917576ab560e7945037", + "version" : "3.3.4" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", + "version" : "1.0.2" + } + } + ], + "version" : 2 } diff --git a/Cryptomator/AddVault/AddVaultCoordinator.swift b/Cryptomator/AddVault/AddVaultCoordinator.swift index 91ebf5db4..5dcd933cb 100644 --- a/Cryptomator/AddVault/AddVaultCoordinator.swift +++ b/Cryptomator/AddVault/AddVaultCoordinator.swift @@ -7,12 +7,14 @@ // import CryptomatorCommonCore +import Dependencies import Foundation import UIKit class AddVaultCoordinator: Coordinator { var childCoordinators = [Coordinator]() var navigationController: UINavigationController + @Dependency(\.fullVersionChecker) private var fullVersionChecker weak var parentCoordinator: MainCoordinator? init(navigationController: UINavigationController) { @@ -76,7 +78,7 @@ class AddVaultCoordinator: Coordinator { } private func isAllowedToCreateNewVault() -> Bool { - return GlobalFullVersionChecker.default.isFullVersion + fullVersionChecker.isFullVersion } } diff --git a/Cryptomator/AddVault/AddVaultSuccessCoordinator.swift b/Cryptomator/AddVault/AddVaultSuccessCoordinator.swift index fde389dc8..e04a118fd 100644 --- a/Cryptomator/AddVault/AddVaultSuccessCoordinator.swift +++ b/Cryptomator/AddVault/AddVaultSuccessCoordinator.swift @@ -28,9 +28,8 @@ class AddVaultSuccessCoordinator: AddVaultSuccesing, Coordinator { let viewModel = AddVaultSuccessViewModel(vaultName: vaultName, vaultUID: vaultUID) let successVC = AddVaultSuccessViewController(viewModel: viewModel) successVC.coordinator = self - navigationController.pushViewController(successVC, animated: true) // Remove the previous ViewControllers so that the user cannot navigate to the previous screens. - navigationController.viewControllers = [successVC] + navigationController.setViewControllers([successVC], animated: true) } // MARK: - AddVaultSuccesing diff --git a/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift b/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift new file mode 100644 index 000000000..92cb4af15 --- /dev/null +++ b/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift @@ -0,0 +1,80 @@ +// +// HubAddVaultCoordinator.swift +// Cryptomator +// +// Created by Philipp Schmid on 16.03.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import AppAuthCore +import CocoaLumberjackSwift +import CryptoKit +import CryptomatorCloudAccessCore +import CryptomatorCommon +import CryptomatorCommonCore +import JOSESwift +import SwiftUI +import UIKit + +class AddHubVaultCoordinator: Coordinator { + var childCoordinators = [Coordinator]() + var navigationController: UINavigationController + let downloadedVaultConfig: DownloadedVaultConfig + let vaultUID: String + let accountUID: String + let vaultItem: VaultItem + let vaultManager: VaultManager + weak var parentCoordinator: Coordinator? + weak var delegate: (VaultInstalling & AnyObject)? + + init(navigationController: UINavigationController, + downloadedVaultConfig: DownloadedVaultConfig, + vaultUID: String, + accountUID: String, + vaultItem: VaultItem, + vaultManager: VaultManager = VaultDBManager.shared) { + self.navigationController = navigationController + self.downloadedVaultConfig = downloadedVaultConfig + self.vaultUID = vaultUID + self.accountUID = accountUID + self.vaultItem = vaultItem + self.vaultManager = vaultManager + } + + func start() { + let unlockHandler = AddHubVaultUnlockHandler(vaultUID: vaultUID, + accountUID: accountUID, vaultItem: vaultItem, + downloadedVaultConfig: downloadedVaultConfig, + vaultManager: vaultManager, + delegate: self) + let child = HubAuthenticationCoordinator(navigationController: navigationController, + vaultConfig: downloadedVaultConfig.vaultConfig, + unlockHandler: unlockHandler, + parent: self, + delegate: self) + childCoordinators.append(child) + child.start() + } +} + +extension AddHubVaultCoordinator: HubVaultUnlockHandlerDelegate { + func successfullyProcessedUnlockedVault() { + delegate?.showSuccessfullyAddedVault(withName: vaultItem.name, vaultUID: vaultUID) + } + + func failedToProcessUnlockedVault(error: Error) { + handleError(error, for: navigationController, onOKTapped: { [weak self] in + self?.parentCoordinator?.childDidFinish(self) + }) + } +} + +extension AddHubVaultCoordinator: HubAuthenticationCoordinatorDelegate { + func userDidCancelHubAuthentication() { + // do nothing as the user already sees the login screen again + } + + func userDismissedHubAuthenticationErrorMessage() { + // do nothing as the user already sees the login screen again + } +} diff --git a/Cryptomator/AddVault/OpenExistingVault/DetectedMasterkeyViewModel.swift b/Cryptomator/AddVault/OpenExistingVault/DetectedMasterkeyViewModel.swift deleted file mode 100644 index b07fdc1be..000000000 --- a/Cryptomator/AddVault/OpenExistingVault/DetectedMasterkeyViewModel.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// DetectedMasterkeyViewModel.swift -// Cryptomator -// -// Created by Philipp Schmid on 27.01.21. -// Copyright © 2021 Skymatic GmbH. All rights reserved. -// - -import CryptomatorCloudAccessCore -import CryptomatorCommonCore -import Foundation - -struct DetectedMasterkeyViewModel { - let masterkeyPath: CloudPath - var text: String { - return String(format: LocalizedString.getValue("addVault.openExistingVault.detectedMasterkey.text"), vaultName) - } - - private var vaultName: String { - let masterkeyParentPath = masterkeyPath.deletingLastPathComponent() - return masterkeyParentPath.lastPathComponent - } -} diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingLegacyVaultPasswordViewModel.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingLegacyVaultPasswordViewModel.swift index e90cfd352..c4f7b9408 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingLegacyVaultPasswordViewModel.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingLegacyVaultPasswordViewModel.swift @@ -12,8 +12,59 @@ import CryptomatorCommonCore import Foundation import Promises -class OpenExistingLegacyVaultPasswordViewModel: OpenExistingVaultPasswordViewModel { - override func addVault() -> Promise { +class OpenExistingLegacyVaultPasswordViewModel: SingleSectionTableViewModel, OpenExistingVaultPasswordViewModelProtocol { + var lastReturnButtonPressed: AnyPublisher { + return setupReturnButtonSupport(for: [passwordCellViewModel], subscribers: &subscribers) + } + + override var title: String? { + return LocalizedString.getValue("addVault.openExistingVault.title") + } + + override var cells: [TableViewCellViewModel] { + return [passwordCellViewModel] + } + + var enableVerifyButton: AnyPublisher { + return passwordCellViewModel.input.$value.map { input in + return !input.isEmpty + }.eraseToAnyPublisher() + } + + let provider: CloudProvider + let account: CloudProviderAccount + + let vault: VaultItem + var vaultName: String { + return vault.name + } + + let vaultUID: String + let passwordCellViewModel = TextFieldCellViewModel(type: .password, isInitialFirstResponder: true) + var password: String { + return passwordCellViewModel.input.value + } + + let downloadedMasterkeyFile: DownloadedMasterkeyFile + + private lazy var subscribers = Set() + + init(provider: CloudProvider, account: CloudProviderAccount, vault: VaultItem, vaultUID: String, downloadedMasterkeyFile: DownloadedMasterkeyFile) { + self.provider = provider + self.account = account + self.vault = vault + self.vaultUID = vaultUID + self.downloadedMasterkeyFile = downloadedMasterkeyFile + } + + func addVault() -> Promise { return VaultDBManager.shared.createLegacyFromExisting(withVaultUID: vaultUID, delegateAccountUID: account.accountUID, vaultItem: vault, password: password, storePasswordInKeychain: false) } + + override func getFooterTitle(for section: Int) -> String? { + guard section == 0 else { + return nil + } + return String(format: LocalizedString.getValue("addVault.openExistingVault.password.footer"), vaultName) + } } diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift index d4a8588f0..aa2d9be69 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift @@ -6,10 +6,13 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import AppAuth import CocoaLumberjackSwift import CryptomatorCloudAccessCore +import CryptomatorCommon import CryptomatorCommonCore import Foundation +import Promises import UIKit class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditAccountBehavior, Coordinator { @@ -121,22 +124,90 @@ private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, Folder } func chooseItem(_ item: Item) { - let viewModel: OpenExistingVaultPasswordViewModelProtocol guard let vaultItem = item as? VaultDetailItem else { handleError(VaultCoordinatorError.wrongItemType, for: navigationController) return } if vaultItem.isLegacyVault { - viewModel = OpenExistingLegacyVaultPasswordViewModel(provider: provider, account: account, vault: vaultItem, vaultUID: UUID().uuidString) + downloadAndProcessExistingLegacyVault(vaultItem) } else { - viewModel = OpenExistingVaultPasswordViewModel(provider: provider, account: account, vault: vaultItem, vaultUID: UUID().uuidString) + downloadAndProcessExistingVault(vaultItem) } + } + private func downloadAndProcessExistingLegacyVault(_ vaultItem: VaultItem) { + let hud = ProgressHUD() + hud.text = LocalizedString.getValue("addVault.openExistingVault.downloadVault.progress") + hud.show(presentingViewController: navigationController) + VaultDBManager.shared.downloadMasterkeyFile(delegateAccountUID: account.accountUID, vaultItem: vaultItem).then { downloadedMasterkeyFile in + all(hud.dismiss(animated: true), Promise(downloadedMasterkeyFile)) + }.then { _, downloadedMasterkeyFile in + self.processDownloadedMasterkeyFile(downloadedMasterkeyFile, vaultItem: vaultItem) + }.catch { error in + hud.dismiss(animated: true).then { + self.handleError(error, for: self.navigationController) + } + } + } + + private func processDownloadedMasterkeyFile(_ downloadedMasterkeyFile: DownloadedMasterkeyFile, vaultItem: VaultItem) { + let viewModel = OpenExistingLegacyVaultPasswordViewModel(provider: provider, + account: account, + vault: vaultItem, + vaultUID: UUID().uuidString, + downloadedMasterkeyFile: downloadedMasterkeyFile) let passwordVC = OpenExistingVaultPasswordViewController(viewModel: viewModel) passwordVC.coordinator = self navigationController.pushViewController(passwordVC, animated: true) } + private func downloadAndProcessExistingVault(_ vaultItem: VaultItem) { + let hud = ProgressHUD() + hud.text = LocalizedString.getValue("addVault.openExistingVault.downloadVault.progress") + hud.show(presentingViewController: navigationController) + VaultDBManager.shared.getUnverifiedVaultConfig(delegateAccountUID: account.accountUID, vaultItem: vaultItem).then { downloadedVaultConfig in + all(hud.dismiss(animated: true), Promise(downloadedVaultConfig)) + }.then { _, downloadedVaultConfig in + self.processDownloadedVaultConfig(downloadedVaultConfig, vaultItem: vaultItem) + }.catch { error in + hud.dismiss(animated: true).then { + self.handleError(error, for: self.navigationController) + } + } + } + + private func processDownloadedVaultConfig(_ downloadedVaultConfig: DownloadedVaultConfig, vaultItem: VaultItem) { + switch VaultConfigHelper.getType(for: downloadedVaultConfig.vaultConfig) { + case .masterkeyFile: + handleMasterkeyFileVaultConfig(downloadedVaultConfig, vaultItem: vaultItem) + case .hub: + handleHubVaultConfig(downloadedVaultConfig, vaultItem: vaultItem) + case .unknown: + handleError(error: VaultProviderFactoryError.unsupportedVaultConfig) + } + } + + private func handleMasterkeyFileVaultConfig(_ downloadedVaultConfig: DownloadedVaultConfig, vaultItem: VaultItem) { + VaultDBManager.shared.downloadMasterkeyFile(delegateAccountUID: account.accountUID, vaultItem: vaultItem).then { downloadedMasterkeyFile in + let viewModel = OpenExistingVaultPasswordViewModel(provider: self.provider, account: self.account, vault: vaultItem, vaultUID: UUID().uuidString, downloadedVaultConfig: downloadedVaultConfig, downloadedMasterkeyFile: downloadedMasterkeyFile) + let passwordVC = OpenExistingVaultPasswordViewController(viewModel: viewModel) + passwordVC.coordinator = self + self.navigationController.pushViewController(passwordVC, animated: true) + } + } + + private func handleHubVaultConfig(_ downloadedVaultConfig: DownloadedVaultConfig, vaultItem: VaultItem) { + let child = AddHubVaultCoordinator(navigationController: navigationController, + downloadedVaultConfig: downloadedVaultConfig, + vaultUID: UUID().uuidString, + accountUID: account.accountUID, + vaultItem: vaultItem) + child.parentCoordinator = self + child.delegate = self + childCoordinators.append(child) + child.start() + } + func showCreateNewFolder(parentPath: CloudPath) {} func handleError(error: Error) { diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultPasswordViewModel.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultPasswordViewModel.swift index 9f5df55cf..e29ba053e 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultPasswordViewModel.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultPasswordViewModel.swift @@ -16,60 +16,22 @@ protocol OpenExistingVaultPasswordViewModelProtocol: SingleSectionTableViewModel var vaultName: String { get } var vaultUID: String { get } var enableVerifyButton: AnyPublisher { get } - // This function is later no longer asynchronous func addVault() -> Promise } -class OpenExistingVaultPasswordViewModel: SingleSectionTableViewModel, OpenExistingVaultPasswordViewModelProtocol { - var lastReturnButtonPressed: AnyPublisher { - return setupReturnButtonSupport(for: [passwordCellViewModel], subscribers: &subscribers) - } - - override var title: String? { - return LocalizedString.getValue("addVault.openExistingVault.title") - } - - override var cells: [TableViewCellViewModel] { - return [passwordCellViewModel] - } - - var enableVerifyButton: AnyPublisher { - return passwordCellViewModel.input.$value.map { input in - return !input.isEmpty - }.eraseToAnyPublisher() - } - - let provider: CloudProvider - let account: CloudProviderAccount - - let vault: VaultItem - var vaultName: String { - return vault.name - } - - let vaultUID: String - let passwordCellViewModel = TextFieldCellViewModel(type: .password, isInitialFirstResponder: true) - var password: String { - return passwordCellViewModel.input.value - } - - private lazy var subscribers = Set() - - init(provider: CloudProvider, account: CloudProviderAccount, vault: VaultItem, vaultUID: String) { - self.provider = provider - self.account = account - self.vault = vault - self.vaultUID = vaultUID - } +class OpenExistingVaultPasswordViewModel: OpenExistingLegacyVaultPasswordViewModel { + let downloadedVaultConfig: DownloadedVaultConfig - func addVault() -> Promise { - return VaultDBManager.shared.createFromExisting(withVaultUID: vaultUID, delegateAccountUID: account.accountUID, vaultItem: vault, password: password, storePasswordInKeychain: false) + init(provider: CloudProvider, account: CloudProviderAccount, vault: VaultItem, vaultUID: String, downloadedVaultConfig: DownloadedVaultConfig, downloadedMasterkeyFile: DownloadedMasterkeyFile) { + self.downloadedVaultConfig = downloadedVaultConfig + super.init(provider: provider, + account: account, + vault: vault, + vaultUID: vaultUID, + downloadedMasterkeyFile: downloadedMasterkeyFile) } - override func getFooterTitle(for section: Int) -> String? { - guard section == 0 else { - return nil - } - return String(format: LocalizedString.getValue("addVault.openExistingVault.password.footer"), vaultName) + override func addVault() -> Promise { + return VaultDBManager.shared.createFromExisting(withVaultUID: vaultUID, delegateAccountUID: account.accountUID, downloadedVaultConfig: downloadedVaultConfig, downloadedMasterkey: downloadedMasterkeyFile, vaultItem: vault, password: password) } } diff --git a/Cryptomator/AppDelegate.swift b/Cryptomator/AppDelegate.swift index 8635e1f1a..729e12a5c 100644 --- a/Cryptomator/AppDelegate.swift +++ b/Cryptomator/AppDelegate.swift @@ -11,6 +11,7 @@ import CryptomatorCloudAccess import CryptomatorCloudAccessCore import CryptomatorCommon import CryptomatorCommonCore +import Dependencies import MSAL import ObjectiveDropboxOfficial import StoreKit @@ -29,22 +30,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { setupIAP() // Set up database - guard let dbURL = CryptomatorDatabase.sharedDBURL else { - // MARK: Handle error + DatabaseManager.shared = DatabaseManager() - DDLogError("dbURL is nil") - return false - } - do { - let dbPool = try CryptomatorDatabase.openSharedDatabase(at: dbURL) - CryptomatorDatabase.shared = try CryptomatorDatabase(dbPool) - DatabaseManager.shared = try DatabaseManager(dbPool: dbPool) - } catch { - // MARK: Handle error - - DDLogError("Initializing CryptomatorDatabase failed with error: \(error)") - return false - } VaultDBManager.shared.recoverMissingFileProviderDomains().catch { error in DDLogError("Recover missing FileProvider domains failed with error: \(error)") } @@ -144,11 +131,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private func setupIAP() { #if ALWAYS_PREMIUM DDLogDebug("Always activated premium") - GlobalFullVersionChecker.default = AlwaysActivatedPremium.default CryptomatorUserDefaults.shared.fullVersionUnlocked = true #else DDLogDebug("Freemium version") - GlobalFullVersionChecker.default = UserDefaultsFullVersionChecker.default + #endif + } +} + +/** + Define the liveValue in the main target since compilation flags do not work on Swift Package Manager level. + Be aware that it is needed to set the default value once per app launch (+ also when launching the FileProviderExtension). + */ +extension FullVersionCheckerKey: DependencyKey { + public static var liveValue: FullVersionChecker { + #if ALWAYS_PREMIUM + return AlwaysActivatedPremium.default + #else + return UserDefaultsFullVersionChecker.default #endif } } diff --git a/Cryptomator/Common/Cells/BindableTableViewCellViewModel.swift b/Cryptomator/Common/Cells/BindableTableViewCellViewModel.swift index 28318c8c7..7c12f3b90 100644 --- a/Cryptomator/Common/Cells/BindableTableViewCellViewModel.swift +++ b/Cryptomator/Common/Cells/BindableTableViewCellViewModel.swift @@ -1,5 +1,5 @@ // -// TableViewCellViewModel.swift +// BindableTableViewCellViewModel.swift // Cryptomator // // Created by Philipp Schmid on 29.07.21. diff --git a/Cryptomator/Common/ChildCoordinator.swift b/Cryptomator/Common/ChildCoordinator.swift index 174634cac..b2a35d185 100644 --- a/Cryptomator/Common/ChildCoordinator.swift +++ b/Cryptomator/Common/ChildCoordinator.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCommonCore import Foundation protocol ChildCoordinator: Coordinator { diff --git a/Cryptomator/Common/CloudAccountList/AccountListViewController.swift b/Cryptomator/Common/CloudAccountList/AccountListViewController.swift index 47f04730b..f02db08d8 100644 --- a/Cryptomator/Common/CloudAccountList/AccountListViewController.swift +++ b/Cryptomator/Common/CloudAccountList/AccountListViewController.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCommon import CryptomatorCommonCore import Foundation import Promises diff --git a/Cryptomator/Common/DatabaseManager.swift b/Cryptomator/Common/DatabaseManager.swift index ae54ed371..1b0d9419b 100644 --- a/Cryptomator/Common/DatabaseManager.swift +++ b/Cryptomator/Common/DatabaseManager.swift @@ -8,20 +8,17 @@ import Combine import CryptomatorCommonCore +import Dependencies import Foundation import GRDB class DatabaseManager { public static var shared: DatabaseManager! - let dbPool: DatabasePool - - init(dbPool: DatabasePool) throws { - self.dbPool = dbPool - } + @Dependency(\.database) private var database func getAllVaults() throws -> [VaultInfo] { - try dbPool.read { db in + try database.read { db in let request = VaultAccount.including(required: VaultAccount.delegateAccount).including(required: VaultAccount.vaultListPosition) return try VaultInfo.fetchAll(db, request) } @@ -35,7 +32,7 @@ class DatabaseManager { for i in tempPositions.indices { tempPositions[i].position = nil } - try dbPool.write { db in + try database.write { db in try db.execute(sql: "PRAGMA ignore_check_constraints=YES") for position in tempPositions { try position.update(db) @@ -51,18 +48,18 @@ class DatabaseManager { let observation = ValueObservation .tracking { try VaultAccount.fetchAll($0) } .removeDuplicates() - return observation.start(in: dbPool, scheduling: .immediate, onError: onError, onChange: onChange) + return observation.start(in: database, scheduling: .immediate, onError: onError, onChange: onChange) } func observeVaultAccount(withVaultUID vaultUID: String, onError: @escaping (Error) -> Void, onChange: @escaping (VaultAccount?) -> Void) -> DatabaseCancellable { let observation = ValueObservation.tracking { db in try VaultAccount.fetchOne(db, key: vaultUID) } - return observation.start(in: dbPool, scheduling: .immediate, onError: onError, onChange: onChange) + return observation.start(in: database, scheduling: .immediate, onError: onError, onChange: onChange) } func getAllAccounts(for cloudProviderType: CloudProviderType) throws -> [AccountInfo] { - try dbPool.read { db in + try database.read { db in let accountWithCloudProviderType = AccountListPosition.account.filter(Column("cloudProviderType") == cloudProviderType) let request = AccountListPosition.including(required: accountWithCloudProviderType).order(Column("position")) return try AccountInfo.fetchAll(db, request) @@ -77,7 +74,7 @@ class DatabaseManager { for i in tempPositions.indices { tempPositions[i].position = nil } - try dbPool.write { db in + try database.write { db in try db.execute(sql: "PRAGMA ignore_check_constraints=YES") for position in tempPositions { try position.update(db) @@ -102,7 +99,7 @@ class DatabaseManager { .removeDuplicates() .map { rows in rows.map(AccountWithDisplayName.init(row:)) } .map { annotatedAccounts in annotatedAccounts.map(\.account) } - return observation.start(in: dbPool, scheduling: .immediate, onError: onError, onChange: onChange) + return observation.start(in: database, scheduling: .immediate, onError: onError, onChange: onChange) } } diff --git a/Cryptomator/Common/LocalWeb/LocalWebViewController.swift b/Cryptomator/Common/LocalWeb/LocalWebViewController.swift index 1f297407b..32084aba1 100644 --- a/Cryptomator/Common/LocalWeb/LocalWebViewController.swift +++ b/Cryptomator/Common/LocalWeb/LocalWebViewController.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCommonCore import Foundation import UIKit import WebKit diff --git a/Cryptomator/Common/PoppingCloseCoordinator.swift b/Cryptomator/Common/PoppingCloseCoordinator.swift index 2331f7f11..baa1fe3c4 100644 --- a/Cryptomator/Common/PoppingCloseCoordinator.swift +++ b/Cryptomator/Common/PoppingCloseCoordinator.swift @@ -6,7 +6,9 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCommonCore import UIKit + protocol PoppingCloseCoordinator: Coordinator { var oldTopViewController: UIViewController? { get } } diff --git a/Cryptomator/Onboarding/OnboardingCoordinator.swift b/Cryptomator/Onboarding/OnboardingCoordinator.swift index 68d9228d6..6a4869569 100644 --- a/Cryptomator/Onboarding/OnboardingCoordinator.swift +++ b/Cryptomator/Onboarding/OnboardingCoordinator.swift @@ -7,12 +7,14 @@ // import CryptomatorCommonCore +import Dependencies import Foundation import UIKit class OnboardingCoordinator: Coordinator { var childCoordinators = [Coordinator]() var navigationController: UINavigationController + @Dependency(\.fullVersionChecker) private var fullVersionChecker init(navigationController: UINavigationController) { self.navigationController = navigationController @@ -25,7 +27,7 @@ class OnboardingCoordinator: Coordinator { } func showIAP() { - guard !GlobalFullVersionChecker.default.isFullVersion else { + guard !fullVersionChecker.isFullVersion else { navigationController.dismiss(animated: true) return } diff --git a/Cryptomator/Onboarding/OnboardingViewController.swift b/Cryptomator/Onboarding/OnboardingViewController.swift index 3a28097eb..2f74cca8f 100644 --- a/Cryptomator/Onboarding/OnboardingViewController.swift +++ b/Cryptomator/Onboarding/OnboardingViewController.swift @@ -1,5 +1,5 @@ // -// OnboardingWelcomeViewController.swift +// OnboardingViewController.swift // Cryptomator // // Created by Tobias Hagemann on 08.09.21. diff --git a/Cryptomator/Resources/about.html b/Cryptomator/Resources/about.html index 4a3e25c06..8c95d5856 100644 --- a/Cryptomator/Resources/about.html +++ b/Cryptomator/Resources/about.html @@ -59,7 +59,9 @@

Third-Party Dependencies

  • ObjectiveDropboxOfficial by Dropbox
  • PCloudSDKSwift by pCloud
  • Promises by Google
  • -
  • swift-log by Apple
  • +
  • Simple Swift Dependencies by Philipp Schmid
  • +
  • SwiftECC by Leif Ibsen
  • +
  • SwiftLog by Apple
  • TPInAppReceipt by Pavel Tikhonenko
  • @@ -67,6 +69,6 @@

    Disclaimer

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    Copyright

    -

    © 2016 – 2023 Skymatic GmbH. All rights reserved.

    +

    © 2016 – 2024 Skymatic GmbH. All rights reserved.

    diff --git a/Cryptomator/S3/S3AuthenticationView.swift b/Cryptomator/S3/S3AuthenticationView.swift index 83c9f2a0b..521aea10b 100644 --- a/Cryptomator/S3/S3AuthenticationView.swift +++ b/Cryptomator/S3/S3AuthenticationView.swift @@ -56,9 +56,7 @@ struct S3AuthenticationView: View { .disableAutocorrection(true) .autocapitalization(.none) } - .introspectTableView(customize: { tableView in - tableView.backgroundColor = .cryptomatorBackground - }) + .setListBackgroundColor(.cryptomatorBackground) } } diff --git a/Cryptomator/S3/S3Authenticator+VC.swift b/Cryptomator/S3/S3Authenticator+VC.swift index f4cd6ea4f..caa9999d0 100644 --- a/Cryptomator/S3/S3Authenticator+VC.swift +++ b/Cryptomator/S3/S3Authenticator+VC.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import CryptomatorCommonCore import Promises import UIKit diff --git a/Cryptomator/Settings/SettingsViewModel.swift b/Cryptomator/Settings/SettingsViewModel.swift index 3fa630c9f..d761538c1 100644 --- a/Cryptomator/Settings/SettingsViewModel.swift +++ b/Cryptomator/Settings/SettingsViewModel.swift @@ -9,6 +9,7 @@ import Combine import CryptomatorCommonCore import CryptomatorFileProvider +import Dependencies import Foundation import Promises import StoreKit @@ -90,13 +91,13 @@ class SettingsViewModel: TableViewModel { return viewModel }() - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector + private var subscribers = Set() private lazy var showDebugModeWarningPublisher = PassthroughSubject() - init(cryptomatorSettings: CryptomatorSettings = CryptomatorUserDefaults.shared, fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared) { + init(cryptomatorSettings: CryptomatorSettings = CryptomatorUserDefaults.shared) { self.cryptomatorSettings = cryptomatorSettings - self.fileProviderConnector = fileProviderConnector } func refreshCacheSize() -> Promise { diff --git a/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift b/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift index fbe93f71b..ad659fb43 100644 --- a/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift +++ b/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift @@ -10,6 +10,7 @@ import CocoaLumberjackSwift import Combine import CryptomatorCommonCore import CryptomatorCryptoLib +import Dependencies import FileProvider import Foundation import Promises @@ -60,27 +61,23 @@ class ChangePasswordViewModel: TableViewModel, ChangePass return _sections } - lazy var cells: [ChangePasswordSection: [BindableTableViewCellViewModel]] = { - return [ - .oldPassword: [oldPasswordCellViewModel], - .newPassword: [newPasswordCellViewModel], - .newPasswordConfirmation: [newPasswordConfirmationCellViewModel] - ] - }() - - private lazy var _sections: [Section] = { - return [ - Section(id: .oldPassword, elements: [oldPasswordCellViewModel]), - Section(id: .newPassword, elements: [newPasswordCellViewModel]), - Section(id: .newPasswordConfirmation, elements: [newPasswordConfirmationCellViewModel]) - ] - }() + lazy var cells: [ChangePasswordSection: [BindableTableViewCellViewModel]] = [ + .oldPassword: [oldPasswordCellViewModel], + .newPassword: [newPasswordCellViewModel], + .newPasswordConfirmation: [newPasswordConfirmationCellViewModel] + ] + + private lazy var _sections: [Section] = [ + Section(id: .oldPassword, elements: [oldPasswordCellViewModel]), + Section(id: .newPassword, elements: [newPasswordCellViewModel]), + Section(id: .newPasswordConfirmation, elements: [newPasswordConfirmationCellViewModel]) + ] private static let minimumPasswordLength = 8 private let vaultAccount: VaultAccount private let domain: NSFileProviderDomain private let vaultManager: VaultManager - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector private let oldPasswordCellViewModel = TextFieldCellViewModel(type: .password, isInitialFirstResponder: true) private let newPasswordCellViewModel = TextFieldCellViewModel(type: .password) @@ -100,11 +97,10 @@ class ChangePasswordViewModel: TableViewModel, ChangePass private lazy var subscribers = Set() - init(vaultAccount: VaultAccount, domain: NSFileProviderDomain, vaultManager: VaultManager = VaultDBManager.shared, fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared) { + init(vaultAccount: VaultAccount, domain: NSFileProviderDomain, vaultManager: VaultManager = VaultDBManager.shared) { self.vaultAccount = vaultAccount self.domain = domain self.vaultManager = vaultManager - self.fileProviderConnector = fileProviderConnector super.init() } diff --git a/Cryptomator/VaultDetail/KeepUnlocked/VaultKeepUnlockedViewModel.swift b/Cryptomator/VaultDetail/KeepUnlocked/VaultKeepUnlockedViewModel.swift index 08e1f36d3..8ed08501e 100644 --- a/Cryptomator/VaultDetail/KeepUnlocked/VaultKeepUnlockedViewModel.swift +++ b/Cryptomator/VaultDetail/KeepUnlocked/VaultKeepUnlockedViewModel.swift @@ -8,6 +8,7 @@ import Combine import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import Promises @@ -40,7 +41,7 @@ class VaultKeepUnlockedViewModel: TableViewModel, Vaul private(set) var keepUnlockedItems = [KeepUnlockedDurationItem]() private let vaultKeepUnlockedSettings: VaultKeepUnlockedSettings private let masterkeyCacheManager: MasterkeyCacheManager - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector private let vaultInfo: VaultInfo private let currentKeepUnlockedDuration: Bindable private var subscriber: AnyCancellable? @@ -48,11 +49,10 @@ class VaultKeepUnlockedViewModel: TableViewModel, Vaul return vaultInfo.vaultUID } - init(currentKeepUnlockedDuration: Bindable, vaultInfo: VaultInfo, vaultKeepUnlockedSettings: VaultKeepUnlockedSettings = VaultKeepUnlockedManager.shared, masterkeyCacheManager: MasterkeyCacheManager = MasterkeyCacheKeychainManager.shared, fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared) { + init(currentKeepUnlockedDuration: Bindable, vaultInfo: VaultInfo, vaultKeepUnlockedSettings: VaultKeepUnlockedSettings = VaultKeepUnlockedManager.shared, masterkeyCacheManager: MasterkeyCacheManager = MasterkeyCacheKeychainManager.shared) { self.vaultInfo = vaultInfo self.vaultKeepUnlockedSettings = vaultKeepUnlockedSettings self.masterkeyCacheManager = masterkeyCacheManager - self.fileProviderConnector = fileProviderConnector self.currentKeepUnlockedDuration = currentKeepUnlockedDuration self.keepUnlockedItems = KeepUnlockedDuration.allCases.map { diff --git a/Cryptomator/VaultDetail/MoveVault/MoveVaultViewModel.swift b/Cryptomator/VaultDetail/MoveVault/MoveVaultViewModel.swift index cffb4c33a..48861fcc1 100644 --- a/Cryptomator/VaultDetail/MoveVault/MoveVaultViewModel.swift +++ b/Cryptomator/VaultDetail/MoveVault/MoveVaultViewModel.swift @@ -9,6 +9,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import GRDB @@ -28,19 +29,17 @@ class MoveVaultViewModel: ChooseFolderViewModel, MoveVaultViewModelProtocol { private let vaultManager: VaultManager private let vaultInfo: VaultInfo private let domain: NSFileProviderDomain - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector init(provider: CloudProvider, currentFolderChoosingCloudPath: CloudPath, vaultInfo: VaultInfo, domain: NSFileProviderDomain, cloudProviderManager: CloudProviderManager = CloudProviderDBManager.shared, - vaultManager: VaultManager = VaultDBManager.shared, - fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared) { + vaultManager: VaultManager = VaultDBManager.shared) { self.vaultInfo = vaultInfo self.domain = domain self.vaultManager = vaultManager - self.fileProviderConnector = fileProviderConnector super.init(canCreateFolder: true, cloudPath: currentFolderChoosingCloudPath, provider: provider) } diff --git a/Cryptomator/VaultDetail/RenameVault/RenameVaultViewModel.swift b/Cryptomator/VaultDetail/RenameVault/RenameVaultViewModel.swift index 4e36d213f..244328848 100644 --- a/Cryptomator/VaultDetail/RenameVault/RenameVaultViewModel.swift +++ b/Cryptomator/VaultDetail/RenameVault/RenameVaultViewModel.swift @@ -9,6 +9,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import GRDB @@ -31,19 +32,18 @@ class RenameVaultViewModel: SetVaultNameViewModel, RenameVaultViewModelProtcol { // swiftlint:disable:next weak_delegate private let delegate: MoveVaultViewModel private let vaultInfo: VaultInfo + @Dependency(\.fileProviderConnector) private var fileProviderConnector init(provider: CloudProvider, vaultInfo: VaultInfo, domain: NSFileProviderDomain, - vaultManager: VaultManager = VaultDBManager.shared, - fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared) { + vaultManager: VaultManager = VaultDBManager.shared) { self.delegate = MoveVaultViewModel( provider: provider, currentFolderChoosingCloudPath: CloudPath("/"), vaultInfo: vaultInfo, domain: domain, - vaultManager: vaultManager, - fileProviderConnector: fileProviderConnector + vaultManager: vaultManager ) self.vaultInfo = vaultInfo } diff --git a/Cryptomator/VaultDetail/VaultDetailUnlockVaultViewModel.swift b/Cryptomator/VaultDetail/VaultDetailUnlockVaultViewModel.swift index faf7451da..83b7d24ae 100644 --- a/Cryptomator/VaultDetail/VaultDetailUnlockVaultViewModel.swift +++ b/Cryptomator/VaultDetail/VaultDetailUnlockVaultViewModel.swift @@ -47,7 +47,7 @@ class VaultDetailUnlockVaultViewModel: SingleSectionTableViewModel, ReturnButton } func unlockVault() throws { - let cachedVault = try VaultDBCache(dbWriter: CryptomatorDatabase.shared.dbPool).getCachedVault(withVaultUID: vault.vaultUID) + let cachedVault = try VaultDBCache().getCachedVault(withVaultUID: vault.vaultUID) let masterkeyFile = try MasterkeyFile.withContentFromData(data: cachedVault.masterkeyFileData) _ = try masterkeyFile.unlock(passphrase: password) try passwordManager.setPassword(password, forVaultUID: vault.vaultUID) diff --git a/Cryptomator/VaultDetail/VaultDetailViewModel.swift b/Cryptomator/VaultDetail/VaultDetailViewModel.swift index 3d098fb69..ef21377cd 100644 --- a/Cryptomator/VaultDetail/VaultDetailViewModel.swift +++ b/Cryptomator/VaultDetail/VaultDetailViewModel.swift @@ -10,6 +10,7 @@ import CocoaLumberjackSwift import Combine import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import GRDB import LocalAuthentication import Promises @@ -73,7 +74,7 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { private let vaultInfo: VaultInfo private let vaultManager: VaultManager - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector private let context = LAContext() private let vaultKeepUnlockedSettings: VaultKeepUnlockedSettings private let passwordManager: VaultPasswordManager @@ -136,12 +137,10 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { } } - private lazy var sectionFooter: [VaultDetailSection: HeaderFooterViewModel] = { - [.vaultInfoSection: VaultDetailInfoFooterViewModel(vault: vaultInfo), - .changeVaultPasswordSection: BaseHeaderFooterViewModel(title: LocalizedString.getValue("vaultDetail.changePassword.footer")), - .lockingSection: unlockSectionFooterViewModel, - .removeVaultSection: BaseHeaderFooterViewModel(title: LocalizedString.getValue("vaultDetail.removeVault.footer"))] - }() + private lazy var sectionFooter: [VaultDetailSection: HeaderFooterViewModel] = [.vaultInfoSection: VaultDetailInfoFooterViewModel(vault: vaultInfo), + .changeVaultPasswordSection: BaseHeaderFooterViewModel(title: LocalizedString.getValue("vaultDetail.changePassword.footer")), + .lockingSection: unlockSectionFooterViewModel, + .removeVaultSection: BaseHeaderFooterViewModel(title: LocalizedString.getValue("vaultDetail.removeVault.footer"))] private lazy var unlockSectionFooterViewModel = UnlockSectionFooterViewModel(vaultUnlocked: vaultInfo.vaultIsUnlocked.value, biometricalUnlockEnabled: biometricalUnlockEnabled, biometryTypeName: context.enrolledBiometricsAuthenticationName(), keepUnlockedDuration: currentKeepUnlockedDuration.value) @@ -156,13 +155,12 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { private var observation: DatabaseCancellable? convenience init(vaultInfo: VaultInfo) { - self.init(vaultInfo: vaultInfo, vaultManager: VaultDBManager.shared, fileProviderConnector: FileProviderXPCConnector.shared, passwordManager: VaultPasswordKeychainManager(), dbManager: DatabaseManager.shared, vaultKeepUnlockedSettings: VaultKeepUnlockedManager.shared) + self.init(vaultInfo: vaultInfo, vaultManager: VaultDBManager.shared, passwordManager: VaultPasswordKeychainManager(), dbManager: DatabaseManager.shared, vaultKeepUnlockedSettings: VaultKeepUnlockedManager.shared) } - init(vaultInfo: VaultInfo, vaultManager: VaultManager, fileProviderConnector: FileProviderConnector, passwordManager: VaultPasswordManager, dbManager: DatabaseManager, vaultKeepUnlockedSettings: VaultKeepUnlockedSettings) { + init(vaultInfo: VaultInfo, vaultManager: VaultManager, passwordManager: VaultPasswordManager, dbManager: DatabaseManager, vaultKeepUnlockedSettings: VaultKeepUnlockedSettings) { self.vaultInfo = vaultInfo self.vaultManager = vaultManager - self.fileProviderConnector = fileProviderConnector self.passwordManager = passwordManager self.title = Bindable(vaultInfo.vaultName) self.vaultKeepUnlockedSettings = vaultKeepUnlockedSettings diff --git a/Cryptomator/VaultList/VaultCellViewModel.swift b/Cryptomator/VaultList/VaultCellViewModel.swift index 0119693f8..a97e41c98 100644 --- a/Cryptomator/VaultList/VaultCellViewModel.swift +++ b/Cryptomator/VaultList/VaultCellViewModel.swift @@ -8,6 +8,7 @@ import Combine import CryptomatorCommonCore +import Dependencies import Promises import UIKit @@ -33,11 +34,10 @@ class VaultCellViewModel: TableViewCellViewModel, VaultCellViewModelProtocol { let vault: VaultInfo private lazy var errorPublisher = PassthroughSubject() - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector - init(vault: VaultInfo, fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared) { + init(vault: VaultInfo) { self.vault = vault - self.fileProviderConnector = fileProviderConnector } func lockVault() -> Promise { diff --git a/Cryptomator/VaultList/VaultListViewController.swift b/Cryptomator/VaultList/VaultListViewController.swift index 8ad7d804d..010251d14 100644 --- a/Cryptomator/VaultList/VaultListViewController.swift +++ b/Cryptomator/VaultList/VaultListViewController.swift @@ -9,6 +9,7 @@ import CocoaLumberjackSwift import Combine import CryptomatorCommonCore +import Dependencies import Foundation import UIKit @@ -17,6 +18,7 @@ class VaultListViewController: ListViewController { private let viewModel: VaultListViewModelProtocol private var observer: NSObjectProtocol? + @Dependency(\.fullVersionChecker) private var fullVersionChecker init(with viewModel: VaultListViewModelProtocol) { self.viewModel = viewModel @@ -60,7 +62,7 @@ class VaultListViewController: ListViewController { super.viewDidAppear(animated) if CryptomatorUserDefaults.shared.showOnboardingAtStartup { coordinator?.showOnboarding() - } else if GlobalFullVersionChecker.default.hasExpiredTrial, !CryptomatorUserDefaults.shared.showedTrialExpiredAtStartup { + } else if fullVersionChecker.hasExpiredTrial, !CryptomatorUserDefaults.shared.showedTrialExpiredAtStartup { coordinator?.showTrialExpired() } } diff --git a/Cryptomator/VaultList/VaultListViewModel.swift b/Cryptomator/VaultList/VaultListViewModel.swift index ed39128ab..4c6254a58 100644 --- a/Cryptomator/VaultList/VaultListViewModel.swift +++ b/Cryptomator/VaultList/VaultListViewModel.swift @@ -9,6 +9,7 @@ import CocoaLumberjackSwift import Combine import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import GRDB @@ -27,7 +28,7 @@ class VaultListViewModel: ViewModel, VaultListViewModelProtocol { var vaultCellViewModels: [VaultCellViewModel] private let dbManager: DatabaseManager private let vaultManager: VaultDBManager - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector private var observation: DatabaseCancellable? private lazy var subscribers = Set() private lazy var errorPublisher = PassthroughSubject() @@ -35,13 +36,12 @@ class VaultListViewModel: ViewModel, VaultListViewModelProtocol { private var removedRow = false convenience init() { - self.init(dbManager: DatabaseManager.shared, vaultManager: VaultDBManager.shared, fileProviderConnector: FileProviderXPCConnector.shared) + self.init(dbManager: DatabaseManager.shared, vaultManager: VaultDBManager.shared) } - init(dbManager: DatabaseManager, vaultManager: VaultDBManager, fileProviderConnector: FileProviderConnector) { + init(dbManager: DatabaseManager, vaultManager: VaultDBManager) { self.dbManager = dbManager self.vaultManager = vaultManager - self.fileProviderConnector = fileProviderConnector self.vaultCellViewModels = [VaultCellViewModel]() } diff --git a/Cryptomator/WebDAV/WebDAVAuthentication.swift b/Cryptomator/WebDAV/WebDAVAuthentication.swift index 1a17f8fa2..02b0cd3b0 100644 --- a/Cryptomator/WebDAV/WebDAVAuthentication.swift +++ b/Cryptomator/WebDAV/WebDAVAuthentication.swift @@ -39,9 +39,7 @@ struct WebDAVAuthentication: View { } .focusedLegacy($focusedField, equals: .password) } - .introspectTableView(customize: { tableView in - tableView.backgroundColor = .cryptomatorBackground - }) + .setListBackgroundColor(.cryptomatorBackground) } } diff --git a/Cryptomator/WebDAV/WebDAVCredentialCoordinator.swift b/Cryptomator/WebDAV/WebDAVCredentialCoordinator.swift index b3468d65b..6b66ddddf 100644 --- a/Cryptomator/WebDAV/WebDAVCredentialCoordinator.swift +++ b/Cryptomator/WebDAV/WebDAVCredentialCoordinator.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import CryptomatorCommonCore import Foundation import UIKit diff --git a/CryptomatorCommon/Package.swift b/CryptomatorCommon/Package.swift index 5617494f4..ecb713a4b 100644 --- a/CryptomatorCommon/Package.swift +++ b/CryptomatorCommon/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.9 // // Package.swift @@ -13,7 +13,7 @@ import PackageDescription let package = Package( name: "CryptomatorCommon", platforms: [ - .iOS(.v13) + .iOS(.v14) ], products: [ .library( @@ -26,22 +26,29 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/cryptomator/cloud-access-swift.git", .upToNextMinor(from: "1.7.0")), - .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack.git", .upToNextMinor(from: "3.8.0")) + .package(url: "https://github.com/cryptomator/cloud-access-swift.git", .upToNextMinor(from: "1.9.0")), + .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack.git", .upToNextMinor(from: "3.8.0")), + .package(url: "https://github.com/PhilLibs/simple-swift-dependencies", .upToNextMajor(from: "0.1.0")), + .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", .upToNextMajor(from: "0.3.0")), + .package(url: "https://github.com/leif-ibsen/SwiftECC", from: "5.0.0") ], targets: [ .target( name: "CryptomatorCommon", dependencies: [ "CryptomatorCommonCore", - "CryptomatorCloudAccess" + .product(name: "CryptomatorCloudAccess", package: "cloud-access-swift") ] ), .target( name: "CryptomatorCommonCore", dependencies: [ - "CocoaLumberjackSwift", - "CryptomatorCloudAccessCore" + .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), + .product(name: "CryptomatorCloudAccessCore", package: "cloud-access-swift"), + .product(name: "Dependencies", package: "simple-swift-dependencies"), + .product(name: "Introspect", package: "SwiftUI-Introspect"), + .product(name: "SwiftUIIntrospect", package: "SwiftUI-Introspect"), + .product(name: "SwiftECC", package: "SwiftECC") ] ), .testTarget( diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift new file mode 100644 index 000000000..566b7ad99 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift @@ -0,0 +1,59 @@ +// +// CryptomatorHubAuthenticator+HubAuthenticating.swift +// +// +// Created by Philipp Schmid on 22.07.22. +// + +import AppAuth +import Base32 +import CryptoKit +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import Dependencies +import UIKit + +enum HubAuthenticationError: Error { + case invalidAuthEndpoint + case invalidTokenEndpoint + case invalidRedirectURL +} + +extension CryptomatorHubAuthenticator: HubAuthenticating { + private static var currentAuthorizationFlow: OIDExternalUserAgentSession? + + public func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState { + guard let authorizationEndpoint = URL(string: hubConfig.authEndpoint) else { + throw HubAuthenticationError.invalidAuthEndpoint + } + guard let tokenEndpoint = URL(string: hubConfig.tokenEndpoint) else { + throw HubAuthenticationError.invalidTokenEndpoint + } + guard let redirectURL = URL(string: "org.cryptomator.ios:/hub/auth") else { + throw HubAuthenticationError.invalidRedirectURL + } + let configuration = OIDServiceConfiguration(authorizationEndpoint: authorizationEndpoint, + tokenEndpoint: tokenEndpoint) + + let request = OIDAuthorizationRequest(configuration: configuration, clientId: hubConfig.clientId, scopes: nil, redirectURL: redirectURL, responseType: OIDResponseTypeCode, additionalParameters: nil) + return try await withCheckedThrowingContinuation({ continuation in + DispatchQueue.main.async { + CryptomatorHubAuthenticator.currentAuthorizationFlow = + OIDAuthState.authState(byPresenting: request, presenting: viewController) { authState, error in + switch (authState, error) { + case let (.some(authState), nil): + continuation.resume(returning: authState) + case let (nil, .some(error)): + continuation.resume(throwing: error) + default: + continuation.resume(throwing: CryptomatorHubAuthenticatorError.unexpectedError) + } + } + } + }) + } +} + +extension HubAuthenticatingKey: DependencyKey { + public static var liveValue: HubAuthenticating = CryptomatorHubAuthenticator() +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/Placeholder.swift b/CryptomatorCommon/Sources/CryptomatorCommon/Placeholder.swift deleted file mode 100644 index 9589047d7..000000000 --- a/CryptomatorCommon/Sources/CryptomatorCommon/Placeholder.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// Placeholder.swift -// CryptomatorCommon -// -// Created by Philipp Schmid on 04.04.21. -// Copyright © 2020 Skymatic GmbH. All rights reserved. -// - -// Workaround to create an "empty" target for SPM diff --git a/Cryptomator/Common/Coordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Coordinator.swift similarity index 72% rename from Cryptomator/Common/Coordinator.swift rename to CryptomatorCommon/Sources/CryptomatorCommonCore/Coordinator.swift index fd78c21e5..dba176a72 100644 --- a/Cryptomator/Common/Coordinator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Coordinator.swift @@ -1,27 +1,29 @@ // // Coordinator.swift -// Cryptomator +// CryptomatorCommon // // Created by Philipp Schmid on 04.01.21. // Copyright © 2021 Skymatic GmbH. All rights reserved. // import CocoaLumberjackSwift -import CryptomatorCommonCore import UIKit -protocol Coordinator: AnyObject { +public protocol Coordinator: AnyObject { var childCoordinators: [Coordinator] { get set } var navigationController: UINavigationController { get set } func start() } -extension Coordinator { - func handleError(_ error: Error, for viewController: UIViewController) { +public extension Coordinator { + func handleError(_ error: Error, for viewController: UIViewController, onOKTapped: (() -> Void)? = nil) { DDLogError("Error: \(error)") let alertController = UIAlertController(title: LocalizedString.getValue("common.alert.error.title"), message: error.localizedDescription, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: LocalizedString.getValue("common.button.ok"), style: .default)) + let okAction = UIAlertAction(title: LocalizedString.getValue("common.button.ok"), style: .default) { _ in + onOKTapped?() + } + alertController.addAction(okAction) viewController.present(alertController, animated: true) } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift index ae543ed4d..e8fcd36ad 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift @@ -6,31 +6,74 @@ // Copyright © 2020 Skymatic GmbH. All rights reserved. // +import CocoaLumberjackSwift +import Dependencies import Foundation import GRDB +private enum CryptomatorDatabaseKey: DependencyKey { + static let liveValue: DatabaseWriter = CryptomatorDatabase.live + + static var testValue: DatabaseWriter { + let inMemoryDB = DatabaseQueue(configuration: .defaultCryptomatorConfiguration) + do { + try CryptomatorDatabase.migrator.migrate(inMemoryDB) + } catch { + DDLogError("Failed to migrate in-memory database: \(error)") + } + return inMemoryDB + } +} + +public extension DependencyValues { + var database: DatabaseWriter { + get { self[CryptomatorDatabaseKey.self] } + set { self[CryptomatorDatabaseKey.self] = newValue } + } +} + +private enum CryptomatorDatabaseLocationKey: DependencyKey { + static var liveValue: URL? { CryptomatorDatabase.sharedDBURL } + static var testValue: URL? { FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: false) } +} + +public extension DependencyValues { + var databaseLocation: URL? { + get { self[CryptomatorDatabaseLocationKey.self] } + set { self[CryptomatorDatabaseLocationKey.self] = newValue } + } +} + public enum CryptomatorDatabaseError: Error { case dbDoesNotExist case incompleteMigration } public class CryptomatorDatabase { - public static var shared: CryptomatorDatabase! + static var live: DatabaseWriter { + @Dependency(\.databaseLocation) var databaseURL - public static var sharedDBURL: URL? { - let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: CryptomatorConstants.appGroupName) - return sharedContainer?.appendingPathComponent("db.sqlite") + guard let dbURL = databaseURL else { + fatalError("Could not get URL for shared database") + } + let database: DatabaseWriter + do { + database = try CryptomatorDatabase.openSharedDatabase(at: dbURL) + } catch { + DDLogError("Failed to open shared database: \(error)") + fatalError("Could not open shared database") + } + do { + try CryptomatorDatabase.migrator.migrate(database) + } catch { + DDLogError("Failed to migrate database: \(error)") + } + return database } - public let dbPool: DatabasePool - private static var oldSharedDBURL: URL? { + static var sharedDBURL: URL? { let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: CryptomatorConstants.appGroupName) - return sharedContainer?.appendingPathComponent("main.sqlite") - } - - public init(_ dbPool: DatabasePool) throws { - self.dbPool = dbPool - try CryptomatorDatabase.migrator.migrate(dbPool) + return sharedContainer?.appendingPathComponent("db.sqlite") } static var migrator: DatabaseMigrator { @@ -44,11 +87,14 @@ public class CryptomatorDatabase { migrator.registerMigration("s3DisplayNameMigration") { db in try s3DisplayNameMigration(db) } + migrator.registerMigration("initialHubSupport") { db in + try initialHubSupportMigration(db) + } return migrator } // swiftlint:disable:next function_body_length - public class func v1Migration(_ db: Database) throws { + class func v1Migration(_ db: Database) throws { // Common try db.create(table: "cloudProviderAccounts") { table in table.column("accountUID", .text).primaryKey() @@ -152,17 +198,22 @@ public class CryptomatorDatabase { """) } + class func initialHubSupportMigration(_ db: Database) throws { + try db.create(table: "hubVaultAccount", body: { table in + table.column("vaultUID", .text).primaryKey().references("vaultAccounts", onDelete: .cascade) + table.column("subscriptionState", .text).notNull() + }) + } + public static func openSharedDatabase(at databaseURL: URL) throws -> DatabasePool { let coordinator = NSFileCoordinator(filePresenter: nil) var coordinatorError: NSError? var dbPool: DatabasePool? var dbError: Error? - var configuration = Configuration() - // Workaround for a SQLite regression (see https://github.com/groue/GRDB.swift/issues/1171 for more details) - configuration.acceptsDoubleQuotedStringLiterals = true + coordinator.coordinate(writingItemAt: databaseURL, options: .forMerging, error: &coordinatorError, byAccessor: { _ in do { - dbPool = try DatabasePool(path: databaseURL.path, configuration: configuration) + dbPool = try DatabasePool(path: databaseURL.path, configuration: .defaultCryptomatorConfiguration) } catch { dbError = error } @@ -193,7 +244,7 @@ public class CryptomatorDatabase { private static func openReadOnlyDatabase(at databaseURL: URL) throws -> DatabasePool { do { - var configuration = Configuration() + var configuration = Configuration.defaultCryptomatorConfiguration configuration.readonly = true let dbPool = try DatabasePool(path: databaseURL.path, configuration: configuration) @@ -211,3 +262,12 @@ public class CryptomatorDatabase { } } } + +extension Configuration { + static var defaultCryptomatorConfiguration: Configuration { + var configuration = Configuration() + // Workaround for a SQLite regression (see https://github.com/groue/GRDB.swift/issues/1171 for more details) + configuration.acceptsDoubleQuotedStringLiterals = true + return configuration + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorErrorView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorErrorView.swift new file mode 100644 index 000000000..07cca92fc --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorErrorView.swift @@ -0,0 +1,26 @@ +import SwiftUI + +public struct CryptomatorErrorView: View { + let text: String? + + public init(text: String? = nil) { + self.text = text + } + + public var body: some View { + VStack(spacing: 20) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 120)) + .foregroundColor(Color(UIColor.cryptomatorYellow)) + if let text { + Text(text) + } + }.padding(.vertical, 20) + } +} + +struct CryptomatorErrorView_Previews: PreviewProvider { + static var previews: some View { + CryptomatorErrorView() + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift index a4907ed79..57e3c887c 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift @@ -29,6 +29,7 @@ class CryptomatorKeychain: CryptomatorKeychainType { static let localFileSystem = CryptomatorKeychain(service: "localFileSystem.auth") static let upgrade = CryptomatorKeychain(service: "upgrade") static let keepUnlocked = CryptomatorKeychain(service: "keepUnlocked") + static let hub = CryptomatorKeychain(service: "hub") init(service: String) { self.service = service diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSimpleButtonView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSimpleButtonView.swift new file mode 100644 index 000000000..79bee3c53 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSimpleButtonView.swift @@ -0,0 +1,37 @@ +import SwiftUI + +struct CryptomatorSimpleButtonView: View { + let buttonTitle: String + let onButtonTap: () -> Void + let headerTitle: String + + var body: some View { + List { + Section { + Button(buttonTitle) { + onButtonTap() + } + } header: { + HStack { + Spacer() + VStack(alignment: .center, spacing: 20) { + Image("bot-vault") + Text(headerTitle) + .textCase(.none) + .foregroundColor(.primary) + .font(.body) + } + .padding(.bottom, 12) + Spacer() + } + } + } + .setListBackgroundColor(.cryptomatorBackground) + } +} + +struct CryptomatorSimpleButtonView_Previews: PreviewProvider { + static var previews: some View { + CryptomatorSimpleButtonView(buttonTitle: "Button", onButtonTap: {}, headerTitle: "Header title.") + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorUserDefaults.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorUserDefaults.swift index c4bf4fab6..cb8b81081 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorUserDefaults.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorUserDefaults.swift @@ -7,6 +7,7 @@ // import CocoaLumberjackSwift +import Dependencies import Foundation public protocol CryptomatorSettings { @@ -16,6 +17,20 @@ public protocol CryptomatorSettings { var hasRunningSubscription: Bool { get set } } +private enum CryptomatorSettingsKey: DependencyKey { + #if DEBUG + static let testValue: CryptomatorSettings = CryptomatorSettingsMock() + #endif + static let liveValue: CryptomatorSettings = CryptomatorUserDefaults.shared +} + +public extension DependencyValues { + var cryptomatorSettings: CryptomatorSettings { + get { self[CryptomatorSettingsKey.self] } + set { self[CryptomatorSettingsKey.self] = newValue } + } +} + public class CryptomatorUserDefaults { public static let shared = CryptomatorUserDefaults() diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift index c394a04b6..3b9bb4fc7 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift @@ -1,5 +1,5 @@ // -// File.swift +// FileProviderConnector.swift // CryptomatorCommonCore // // Created by Philipp Schmid on 26.07.21. @@ -7,6 +7,7 @@ // import CocoaLumberjackSwift +import Dependencies import FileProvider import Foundation import Promises @@ -46,6 +47,20 @@ public extension FileProviderConnector { } } +private enum FileProviderConnectorKey: DependencyKey { + static var liveValue: FileProviderConnector { FileProviderXPCConnector() } + #if DEBUG + static var testValue: FileProviderConnector = UnimplementedFileProviderConnector() + #endif +} + +public extension DependencyValues { + var fileProviderConnector: FileProviderConnector { + get { self[FileProviderConnectorKey.self] } + set { self[FileProviderConnectorKey.self] = newValue } + } +} + public struct XPC { public let proxy: T let doneHandler: () -> Void @@ -69,8 +84,6 @@ public class FileProviderXPCConnector: FileProviderConnector { } } - public static let shared = FileProviderXPCConnector() - public func getXPC(serviceName: NSFileProviderServiceName, domain: NSFileProviderDomain?) -> Promise> { var url = NSFileProviderManager.default.documentStorageURL if let domain = domain { @@ -119,3 +132,17 @@ public extension XPC { self.init(proxy: proxy, doneHandler: {}) } } + +#if DEBUG +private struct UnimplementedFileProviderConnector: FileProviderConnector { + func getXPC(serviceName: NSFileProviderServiceName, domain: NSFileProviderDomain?) -> Promise> { + unimplemented("\(Self.self).getXPC(serviceName:domain:) not implemented", placeholder: Promise(UnimplementedError())) + } + + func getXPC(serviceName: NSFileProviderServiceName, domainIdentifier: NSFileProviderDomainIdentifier) -> Promise> { + unimplemented("\(Self.self).getXPC(serviceName:domainIdentifier:) not implemented", placeholder: Promise(UnimplementedError())) + } + + private struct UnimplementedError: Error {} +} +#endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultUnlocking.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultUnlocking.swift index 783ebb863..143b54ba1 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultUnlocking.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultUnlocking.swift @@ -8,11 +8,41 @@ import FileProvider import Foundation +import Promises + @objc public protocol VaultUnlocking: NSFileProviderServiceSource { // "Because communication over XPC is asynchronous, all methods in the protocol must have a return type of void. If you need to return data, you can define a reply block [...]" see: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html func unlockVault(kek: [UInt8], reply: @escaping (NSError?) -> Void) func startBiometricalUnlock() func endBiometricalUnlock() + + func unlockVault(rawKey: [UInt8], reply: @escaping (NSError?) -> Void) +} + +public extension VaultUnlocking { + func unlockVault(kek: [UInt8]) -> Promise { + return Promise { fulfill, reject in + self.unlockVault(kek: kek) { error in + if let error = error { + reject(error) + } else { + fulfill(()) + } + } + } + } + + func unlockVault(rawKey: [UInt8]) -> Promise { + return Promise { fulfill, reject in + self.unlockVault(rawKey: rawKey) { error in + if let error = error { + reject(error) + } else { + fulfill(()) + } + } + } + } } public extension NSFileProviderServiceName { diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/FullVersionChecker.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/FullVersionChecker.swift index 0db539a38..9e2e5cac0 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/FullVersionChecker.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/FullVersionChecker.swift @@ -6,27 +6,31 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import Dependencies import Foundation + public protocol FullVersionChecker { var isFullVersion: Bool { get } var hasExpiredTrial: Bool { get } } -/** - Use a singleton to inject the full version checker conveniently at several initializers since compilation flags do not work on Swift Package Manager level. - Be aware that it is needed to set the default value once per app launch (+ also when launching the FileProviderExtension). - */ -public enum GlobalFullVersionChecker { - public static var `default`: FullVersionChecker! +public enum FullVersionCheckerKey {} + +extension FullVersionCheckerKey: TestDependencyKey { + public static let testValue: FullVersionChecker = FullVersionCheckerMock() +} + +public extension DependencyValues { + var fullVersionChecker: FullVersionChecker { + get { self[FullVersionCheckerKey.self] } + set { self[FullVersionCheckerKey.self] = newValue } + } } public class UserDefaultsFullVersionChecker: FullVersionChecker { - public static let `default` = UserDefaultsFullVersionChecker(cryptomatorSettings: CryptomatorUserDefaults.shared) - private let cryptomatorSettings: CryptomatorSettings + @Dependency(\.cryptomatorSettings) private var cryptomatorSettings - init(cryptomatorSettings: CryptomatorSettings) { - self.cryptomatorSettings = cryptomatorSettings - } + public static let `default` = UserDefaultsFullVersionChecker() public var isFullVersion: Bool { if cryptomatorSettings.fullVersionUnlocked { diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift new file mode 100644 index 000000000..94877a1b7 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -0,0 +1,386 @@ +// +// CryptomatorHubAuthenticator.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 22.07.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import AppAuthCore +import CryptoKit +import CryptomatorCloudAccessCore +import CryptomatorCryptoLib +import Dependencies +import Foundation +import JOSESwift + +public enum HubAuthenticationFlow { + case success(HubAuthenticationFlowSuccess) + case accessNotGranted + case needsDeviceRegistration + case licenseExceeded + case requiresAccountInitialization(at: URL) +} + +public struct HubAuthenticationFlowSuccess { + public let encryptedUserKey: JWE + public let encryptedVaultKey: JWE + public let header: [AnyHashable: Any] +} + +public enum CryptomatorHubAuthenticatorError: Error { + case unexpectedError + case unexpectedResponse + case deviceNameAlreadyExists + + case unexpectedPrivateKeyFormat + case invalidVaultConfig + case invalidHubConfig + case invalidBaseURL + case invalidDeviceResourceURL + case missingAccessToken + case incompatibleHubVersion +} + +public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving { + private static let scheme = "hub+" + private static let minimumHubVersion = 2 + @Dependency(\.cryptomatorHubKeyProvider) private var cryptomatorHubKeyProvider + + public init() {} + + public func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow { + guard let hubConfig = vaultConfig.allegedHubConfig, let vaultBaseURL = getVaultBaseURL(from: vaultConfig) else { + throw CryptomatorHubAuthenticatorError.invalidVaultConfig + } + + guard let apiBaseURL = hubConfig.getAPIBaseURL(), let webAppURL = hubConfig.getWebAppURL() else { + throw CryptomatorHubAuthenticatorError.invalidHubConfig + } + + guard try await hubInstanceHasMinimumAPILevel(of: Self.minimumHubVersion, apiBaseURL: apiBaseURL, authState: authState) else { + throw CryptomatorHubAuthenticatorError.incompatibleHubVersion + } + + let retrieveMasterkeyResponse = try await getVaultMasterKey(vaultBaseURL: vaultBaseURL, + authState: authState, + webAppURL: webAppURL) + + let encryptedVaultKey: String + let unlockHeader: [AnyHashable: Any] + switch retrieveMasterkeyResponse { + case let .success(key, header): + encryptedVaultKey = key + unlockHeader = header + case .accessNotGranted: + return .accessNotGranted + case .licenseExceeded: + return .licenseExceeded + case let .requiresAccountInitialization(profileURL): + return .requiresAccountInitialization(at: profileURL) + case .legacyHubVersion: + throw CryptomatorHubAuthenticatorError.incompatibleHubVersion + } + + let retrieveUserPrivateKeyResponse = try await getUserKey(apiBaseURL: apiBaseURL, authState: authState) + + let encryptedUserKey: String + switch retrieveUserPrivateKeyResponse { + case let .unlockedSucceeded(deviceDto): + encryptedUserKey = deviceDto.userPrivateKey + case .deviceSetup: + return .needsDeviceRegistration + } + + let encryptedUserKeyJWE = try JWE(compactSerialization: encryptedUserKey) + let encryptedVaultKeyJWE = try JWE(compactSerialization: encryptedVaultKey) + + return .success(.init(encryptedUserKey: encryptedUserKeyJWE, encryptedVaultKey: encryptedVaultKeyJWE, header: unlockHeader)) + } + + /** + Registers a new device. + + Registers a new mobile device at the Hub instance derived from the `hubConfig` with the given `name`. + + The device registration consists of two requests: + + 1. Request the encrypted user key which can be decrypted by using the `setupCode`. + 2. Send a Create Device request to the Hub instance which contains the user key encrypted with the device key pair. + */ + public func registerDevice(withName name: String, + hubConfig: HubConfig, + authState: OIDAuthState, + setupCode: String) async throws { + guard let apiBaseURL = hubConfig.getAPIBaseURL() else { + throw CryptomatorHubAuthenticatorError.invalidBaseURL + } + + let userDto = try await getUser(apiBaseURL: apiBaseURL, authState: authState) + + let publicKey = try cryptomatorHubKeyProvider.getPublicKey() + + let encryptedUserKeyJWE = try getEncryptedUserKeyJWE(userDto: userDto, setupCode: setupCode, publicKey: publicKey) + + let deviceID = try getDeviceID() + let derPubKey = publicKey.derRepresentation + + let now = ISO8601DateFormatter().string(from: Date()) + + let dto = CreateDeviceDto(id: deviceID, + name: name, + type: "MOBILE", + publicKey: derPubKey.base64EncodedString(), + userPrivateKey: encryptedUserKeyJWE.compactSerializedString, + creationTime: now) + + try await createDevice(dto, apiBaseURL: apiBaseURL, authState: authState) + } + + private func getUser(apiBaseURL: URL, authState: OIDAuthState) async throws -> UserDto { + let url = apiBaseURL.appendingPathComponent("users/me") + let (accessToken, _) = try await authState.performAction() + guard let accessToken = accessToken else { + throw CryptomatorHubAuthenticatorError.missingAccessToken + } + var request = URLRequest(url: url) + request.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] + let (data, response) = try await URLSession.shared.data(with: request) + let httpResponse = response as? HTTPURLResponse + guard httpResponse?.statusCode == 200 else { + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + return try JSONDecoder().decode(UserDto.self, from: data) + } + + private func getEncryptedUserKeyJWE(userDto: UserDto, setupCode: String, publicKey: P384.KeyAgreement.PublicKey) throws -> JWE { + guard let privateKey = userDto.privateKey.data(using: .utf8) else { + throw CryptomatorHubAuthenticatorError.unexpectedPrivateKeyFormat + } + let jwe = try JWE(compactSerialization: privateKey) + + let userKey = try JWEHelper.decryptUserKey(jwe: jwe, setupCode: setupCode) + + return try JWEHelper.encryptUserKey(userKey: userKey, deviceKey: publicKey) + } + + private func createDevice(_ dto: CreateDeviceDto, apiBaseURL: URL, authState: OIDAuthState) async throws { + let deviceResourceURL = apiBaseURL.appendingPathComponent("devices") + let deviceURL = deviceResourceURL.appendingPathComponent(dto.id) + + var request = URLRequest(url: deviceURL) + request.httpMethod = "PUT" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode(dto) + + let (accessToken, _) = try await authState.performAction() + guard let secondAccessToken = accessToken else { + throw CryptomatorHubAuthenticatorError.missingAccessToken + } + request.allHTTPHeaderFields = ["Authorization": "Bearer \(secondAccessToken)"] + + let (_, response) = try await URLSession.shared.data(with: request) + + switch (response as? HTTPURLResponse)?.statusCode { + case 201: + break + case 409: + throw CryptomatorHubAuthenticatorError.deviceNameAlreadyExists + default: + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + } + + private func getVaultBaseURL(from vaultConfig: UnverifiedVaultConfig) -> URL? { + guard let keyId = vaultConfig.keyId, keyId.hasPrefix(CryptomatorHubAuthenticator.scheme) else { + return nil + } + let baseURLPath = keyId.deletingPrefix(CryptomatorHubAuthenticator.scheme) + return URL(string: baseURLPath) + } + + private func getDeviceID() throws -> String { + let publicKey = try cryptomatorHubKeyProvider.getPublicKey() + let digest = SHA256.hash(data: publicKey.derRepresentation) + return digest.data.base16EncodedString + } + + /** + Checks if the Hub instance at `apiBaseURL` has at least the API level of `minimumLevel`. + + - Note: The legacy Hub which is not supported returns a 0. + */ + private func hubInstanceHasMinimumAPILevel(of minimumLevel: Int, apiBaseURL: URL, authState: OIDAuthState) async throws -> Bool { + let url = apiBaseURL.appendingPathComponent("config") + let (accessToken, _) = try await authState.performAction() + guard let accessToken = accessToken else { + throw CryptomatorHubAuthenticatorError.missingAccessToken + } + var request = URLRequest(url: url) + request.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] + let (data, response) = try await URLSession.shared.data(with: request) + + guard (response as? HTTPURLResponse)?.statusCode == 200 else { + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + let config = try JSONDecoder().decode(APIConfigDto.self, from: data) + return config.apiLevel >= minimumLevel + } + + private func getVaultMasterKey(vaultBaseURL: URL, authState: OIDAuthState, webAppURL: URL) async throws -> RetrieveVaultMasterkeyEncryptedForUserResponse { + let url = vaultBaseURL.appendingPathComponent("access-token") + let (accessToken, _) = try await authState.performAction() + guard let accessToken = accessToken else { + throw CryptomatorHubAuthenticatorError.missingAccessToken + } + var urlRequest = URLRequest(url: url) + urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] + let (data, response) = try await URLSession.shared.data(with: urlRequest) + let httpResponse = response as? HTTPURLResponse + switch httpResponse?.statusCode { + case 200: + guard let body = String(data: data, encoding: .utf8) else { + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + return .success(encryptedVaultKey: body, header: httpResponse?.allHeaderFields ?? [:]) + case 402: + return .licenseExceeded + case 403, 410: + return .accessNotGranted + case 404: + return .legacyHubVersion + case 449: + let profileURL = webAppURL.appendingPathComponent("profile") + return .requiresAccountInitialization(at: profileURL) + default: + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + } + + private func getUserKey(apiBaseURL: URL, authState: OIDAuthState) async throws -> RetrieveUserEncryptedPKResponse { + let deviceID = try getDeviceID() + let url = apiBaseURL.appendingPathComponent("devices").appendingPathComponent(deviceID) + let (accessToken, _) = try await authState.performAction() + guard let accessToken = accessToken else { + throw CryptomatorHubAuthenticatorError.missingAccessToken + } + var urlRequest = URLRequest(url: url) + urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] + let (data, response) = try await URLSession.shared.data(with: urlRequest) + let httpResponse = response as? HTTPURLResponse + + switch httpResponse?.statusCode { + case 200: + return try .unlockedSucceeded(JSONDecoder().decode(DeviceDto.self, from: data)) + case 404: + return .deviceSetup + default: + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + } + + struct CreateDeviceDto: Codable { + let id: String + let name: String + let type: String + let publicKey: String + let userPrivateKey: String + let creationTime: String + } + + private struct APIConfigDto: Codable { + let apiLevel: Int + } + + private enum RetrieveUserEncryptedPKResponse { + // 200 + case unlockedSucceeded(DeviceDto) + // 404 + case deviceSetup + } + + private enum RetrieveVaultMasterkeyEncryptedForUserResponse { + // 200 + case success(encryptedVaultKey: String, header: [AnyHashable: Any]) + // 403, 410 + case accessNotGranted + // 402 + case licenseExceeded + // 449 + case requiresAccountInitialization(at: URL) + // 404 + case legacyHubVersion + } + + private struct DeviceDto: Codable { + let userPrivateKey: String + } +} + +extension URLSession { + @available(iOS, deprecated: 15.0, message: "This extension is no longer necessary. Use API built into SDK") + func data(with request: URLRequest) async throws -> (Data, URLResponse) { + try await withCheckedThrowingContinuation { continuation in + let task = self.dataTask(with: request) { data, response, error in + guard let data = data, let response = response else { + let error = error ?? URLError(.badServerResponse) + return continuation.resume(throwing: error) + } + + continuation.resume(returning: (data, response)) + } + + task.resume() + } + } +} + +extension Digest { + var bytes: [UInt8] { Array(makeIterator()) } + var data: Data { Data(bytes) } +} + +extension OIDAuthState { + func performAction() async throws -> (String?, String?) { + try await withCheckedThrowingContinuation({ continuation in + performAction { accessToken, idToken, error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: (accessToken, idToken)) + } + } + }) + } +} + +extension String { + func deletingPrefix(_ prefix: String) -> String { + guard hasPrefix(prefix) else { return self } + return String(dropFirst(prefix.count)) + } +} + +extension HubConfig { + func getAPIBaseURL() -> URL? { + if let apiBaseUrl { + return URL(string: apiBaseUrl) + } + guard let deviceResourceURL = URL(string: devicesResourceUrl) else { + return nil + } + return deviceResourceURL.deletingLastPathComponent() + } + + func getWebAppURL() -> URL? { + getAPIBaseURL()?.deletingLastPathComponent().appendingPathComponent("app") + } +} + +private struct UserDto: Codable { + let id: String + let name: String + let publicKey: String + let privateKey: String + let setupCode: String +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubKeyProvider.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubKeyProvider.swift new file mode 100644 index 000000000..845a96232 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubKeyProvider.swift @@ -0,0 +1,108 @@ +// +// CryptomatorHubKeyProvider.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 20.07.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import CryptoKit +import Dependencies +import Foundation + +protocol CryptomatorHubKeyProvider { + func getPublicKey() throws -> P384.KeyAgreement.PublicKey + + func getPrivateKey() throws -> P384.KeyAgreement.PrivateKey +} + +private enum CryptomatorHubKeyProviderKey: DependencyKey { + static let liveValue: CryptomatorHubKeyProvider = CryptomatorHubKeyProviderImpl(keychain: CryptomatorKeychain.hub) + #if DEBUG + static let testValue: CryptomatorHubKeyProvider = CryptomatorHubKeyProviderMock() + #endif +} + +extension DependencyValues { + var cryptomatorHubKeyProvider: CryptomatorHubKeyProvider { + get { self[CryptomatorHubKeyProviderKey.self] } + set { self[CryptomatorHubKeyProviderKey.self] = newValue } + } +} + +public struct CryptomatorHubKeyProviderImpl: CryptomatorHubKeyProvider { + public static let shared: CryptomatorHubKeyProviderImpl = .init(keychain: CryptomatorKeychain.hub) + let keychain: CryptomatorKeychainType + private let keychainKey = "privateKey" + + public func getPublicKey() throws -> P384.KeyAgreement.PublicKey { + let privateKey = try getPrivateKey() + return privateKey.publicKey + } + + public func getPrivateKey() throws -> P384.KeyAgreement.PrivateKey { + let privateKey: P384.KeyAgreement.PrivateKey + if let existingKeyData = keychain.getAsData(keychainKey) { + privateKey = try P384.KeyAgreement.PrivateKey(rawRepresentation: existingKeyData) + } else { + privateKey = P384.KeyAgreement.PrivateKey(compactRepresentable: false) + try saveKey(privateKey) + } + return privateKey + } + + private func saveKey(_ privateKey: P384.KeyAgreement.PrivateKey) throws { + try keychain.set(keychainKey, value: privateKey.rawRepresentation) + } + + public func delete() { + try? keychain.delete(keychainKey) + } +} + +#if DEBUG + +// MARK: - CryptomatorHubKeyProviderMock - + +// swiftlint: disable all +final class CryptomatorHubKeyProviderMock: CryptomatorHubKeyProvider { + // MARK: - getPublicKey + + var getPublicKeyThrowableError: Error? + var getPublicKeyCallsCount = 0 + var getPublicKeyCalled: Bool { + getPublicKeyCallsCount > 0 + } + + var getPublicKeyReturnValue: P384.KeyAgreement.PublicKey! + var getPublicKeyClosure: (() throws -> P384.KeyAgreement.PublicKey)? + + func getPublicKey() throws -> P384.KeyAgreement.PublicKey { + if let error = getPublicKeyThrowableError { + throw error + } + getPublicKeyCallsCount += 1 + return try getPublicKeyClosure.map({ try $0() }) ?? getPublicKeyReturnValue + } + + // MARK: - getPrivateKey + + var getPrivateKeyThrowableError: Error? + var getPrivateKeyCallsCount = 0 + var getPrivateKeyCalled: Bool { + getPrivateKeyCallsCount > 0 + } + + var getPrivateKeyReturnValue: P384.KeyAgreement.PrivateKey! + var getPrivateKeyClosure: (() throws -> P384.KeyAgreement.PrivateKey)? + + func getPrivateKey() throws -> P384.KeyAgreement.PrivateKey { + if let error = getPrivateKeyThrowableError { + throw error + } + getPrivateKeyCallsCount += 1 + return try getPrivateKeyClosure.map({ try $0() }) ?? getPrivateKeyReturnValue + } +} +// swiftlint: enable all +#endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticating.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticating.swift new file mode 100644 index 000000000..c43627ce5 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticating.swift @@ -0,0 +1,25 @@ +import AppAuthCore +import CryptomatorCloudAccessCore +import Dependencies +import UIKit + +public protocol HubAuthenticating { + func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState +} + +public enum HubAuthenticatingKey: TestDependencyKey { + public static var testValue: HubAuthenticating = UnimplementedHubAuthenticatingService() +} + +public extension DependencyValues { + var hubAuthenticationService: HubAuthenticating { + get { self[HubAuthenticatingKey.self] } + set { self[HubAuthenticatingKey.self] = newValue } + } +} + +struct UnimplementedHubAuthenticatingService: HubAuthenticating { + func authenticate(with hubConfig: CryptomatorCloudAccessCore.HubConfig, from viewController: UIViewController) async throws -> OIDAuthState { + unimplemented(placeholder: OIDAuthState(authorizationResponse: .init(request: .init(configuration: .init(authorizationEndpoint: URL(string: "example.com")!, tokenEndpoint: URL(string: "example.com")!), clientId: "", scopes: nil, redirectURL: URL(string: "example.com")!, responseType: "code", additionalParameters: nil), parameters: [:]))) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift new file mode 100644 index 000000000..008c73b76 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift @@ -0,0 +1,122 @@ +import AppAuthCore +import CryptomatorCloudAccessCore +import Dependencies +import SwiftUI +import UIKit + +public protocol HubAuthenticationCoordinatorDelegate: AnyObject { + @MainActor + func userDidCancelHubAuthentication() + + @MainActor + func userDismissedHubAuthenticationErrorMessage() +} + +public final class HubAuthenticationCoordinator: Coordinator { + public var childCoordinators = [Coordinator]() + public var navigationController: UINavigationController + public weak var parent: Coordinator? + + private let vaultConfig: UnverifiedVaultConfig + private var progressHUD: ProgressHUD? + private let unlockHandler: HubVaultUnlockHandler + @Dependency(\.hubAuthenticationService) var hubAuthenticator + private weak var delegate: HubAuthenticationCoordinatorDelegate? + + public init(navigationController: UINavigationController, + vaultConfig: UnverifiedVaultConfig, + unlockHandler: HubVaultUnlockHandler, + parent: Coordinator?, + delegate: HubAuthenticationCoordinatorDelegate) { + self.navigationController = navigationController + self.vaultConfig = vaultConfig + self.unlockHandler = unlockHandler + self.parent = parent + self.delegate = delegate + } + + public func start() { + guard let hubConfig = vaultConfig.allegedHubConfig else { + handleError(HubAuthenticationViewModelError.missingHubConfig, for: navigationController, onOKTapped: { [weak self] in + guard let self else { return } + parent?.childDidFinish(self) + }) + return + } + Task { @MainActor in + let authenticator = HubUserAuthenticator(hubAuthenticator: hubAuthenticator, viewController: navigationController) + let authState: OIDAuthState + do { + authState = try await authenticator.authenticate(with: hubConfig) + } catch let error as NSError where error.domain == OIDGeneralErrorDomain && error.code == OIDErrorCode.userCanceledAuthorizationFlow.rawValue { + // do not show alert if user canceled it on purpose + delegate?.userDidCancelHubAuthentication() + parent?.childDidFinish(self) + return + } catch { + handleError(error, for: navigationController, onOKTapped: { [weak self] in + guard let self else { return } + delegate?.userDismissedHubAuthenticationErrorMessage() + parent?.childDidFinish(self) + }) + return + } + let viewModel = HubAuthenticationViewModel(authState: authState, + vaultConfig: vaultConfig, + unlockHandler: unlockHandler, + delegate: self) + await viewModel.continueToAccessCheck() + guard !viewModel.isLoggedIn else { + // Do not show the authentication view if the user already authenticated successfully + return + } + navigationController.setNavigationBarHidden(false, animated: false) + let viewController = HubAuthenticationViewController(viewModel: viewModel) + navigationController.pushViewController(viewController, animated: true) + } + } + + private func showProgressHUD() { + assert(progressHUD == nil, "showProgressHUD called although one is already shown") + progressHUD = ProgressHUD() + progressHUD?.show(presentingViewController: navigationController) + progressHUD?.showLoadingIndicator() + } + + private func hideProgressHUD() async { + await withCheckedContinuation { continuation in + guard let progressHUD else { + continuation.resume() + return + } + progressHUD.dismiss(animated: true, completion: { [weak self] in + continuation.resume() + self?.progressHUD = nil + }) + } + } +} + +extension HubAuthenticationCoordinator: HubAuthenticationViewModelDelegate { + public func hubAuthenticationViewModelWantsToShowLoadingIndicator() { + showProgressHUD() + } + + public func hubAuthenticationViewModelWantsToHideLoadingIndicator() async { + await hideProgressHUD() + } + + public func hubAuthenticationViewModelWantsToShowNeedsAccountInitAlert(profileURL: URL) { + let alertController = UIAlertController(title: LocalizedString.getValue("hubAuthentication.requireAccountInit.alert.title"), + message: LocalizedString.getValue("hubAuthentication.requireAccountInit.alert.message"), + preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: LocalizedString.getValue("common.button.cancel"), style: .cancel)) + let goToProfileAction = UIAlertAction(title: LocalizedString.getValue("hubAuthentication.requireAccountInit.alert.actionButton"), + style: .default, + handler: { _ in UIApplication.shared.open(profileURL) }) + alertController.addAction(goToProfileAction) + + navigationController.present(alertController, animated: true) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift new file mode 100644 index 000000000..8269b4726 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift @@ -0,0 +1,12 @@ +import CryptoKit +import JOSESwift + +public protocol HubAuthenticationFlowDelegate: AnyObject { + func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async +} + +public struct HubUnlockResponse { + public let jwe: JWE + public let privateKey: P384.KeyAgreement.PrivateKey + public let subscriptionState: HubSubscriptionState +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift new file mode 100644 index 000000000..77e3c2815 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift @@ -0,0 +1,48 @@ +import SwiftUI + +public struct HubAuthenticationView: View { + @ObservedObject var viewModel: HubAuthenticationViewModel + + public init(viewModel: HubAuthenticationViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + ZStack { + Color.cryptomatorBackground + .ignoresSafeArea() + VStack(spacing: 20) { + switch viewModel.authenticationFlowState { + case .deviceRegistration: + HubDeviceRegistrationView( + deviceName: $viewModel.deviceName, + accountKey: $viewModel.setupCode, + onRegisterTap: { Task { await viewModel.register() }} + ) + case .accessNotGranted: + HubAccessNotGrantedView(onRefresh: { Task { await viewModel.refresh() }}) + case .licenseExceeded: + CryptomatorErrorView(text: LocalizedString.getValue("hubAuthentication.licenseExceeded")) + case let .error(description): + CryptomatorErrorView(text: description) + case .none: + EmptyView() + } + } + .padding() + .navigationTitle(LocalizedString.getValue("hubAuthentication.title")) + .alert( + isPresented: .init( + get: { viewModel.authenticationFlowState == .deviceRegistration(.needsAuthorization) }, + set: { _ in Task { await viewModel.continueToAccessCheck() }} + ) + ) { + Alert( + title: Text(LocalizedString.getValue("hubAuthentication.deviceRegistration.needsAuthorization.alert.title")), + message: Text(LocalizedString.getValue("hubAuthentication.deviceRegistration.needsAuthorization.alert.message")), + dismissButton: .default(Text(LocalizedString.getValue("common.button.ok"))) + ) + } + } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewController.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewController.swift new file mode 100644 index 000000000..25152feb3 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewController.swift @@ -0,0 +1,73 @@ +import Combine +import Foundation +import SwiftUI + +/** + ViewController for the `HubAuthenticationView`. + + This ViewController build the bridge between UIKit and the SwiftUI `HubAuthenticationView`. + This bridge is needed to show the tool bar items of `HubAuthenticationView` in a UIKit `UINavigationController`. + */ +public class HubAuthenticationViewController: UIViewController { + private let viewModel: HubAuthenticationViewModel + private var cancellables = Set() + + public init(viewModel: HubAuthenticationViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + title = LocalizedString.getValue("hubAuthentication.title") + + setupToolBar() + setupSwiftUIView() + } + + private func setupSwiftUIView() { + let child = UIHostingController(rootView: HubAuthenticationView(viewModel: viewModel)) + addChild(child) + view.addSubview(child.view) + child.didMove(toParent: self) + child.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate(child.view.constraints(equalTo: view)) + } + + private func setupToolBar() { + if let initialState = viewModel.authenticationFlowState { + updateToolbar(state: initialState) + } + + viewModel.$authenticationFlowState + .compactMap { $0 } + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] in + self?.updateToolbar(state: $0) + }) + .store(in: &cancellables) + } + + /** + Updates the `UINavigationItem` based on the given `state`. + - Note: This solution is far from ideal as we need to update the content of the tool bar in two places, i.e. in this method and inside the SwiftUI itself. Otherwise the behavior can differ when used inside a UINavigationController and a "SwiftUI native" `NavigationView`/ `NavigationStackView`. + */ + private func updateToolbar(state: HubAuthenticationViewModel.State) { + switch state { + case .deviceRegistration: + let registerButton = UIBarButtonItem(title: "Register", style: .done, target: self, action: #selector(registerButtonTapped)) + navigationItem.rightBarButtonItem = registerButton + default: + navigationItem.rightBarButtonItem = nil + } + } + + @objc private func registerButtonTapped() { + Task { await viewModel.register() } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift new file mode 100644 index 000000000..ee81e7354 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -0,0 +1,158 @@ +import AppAuthCore +import CocoaLumberjackSwift +import CryptoKit +import CryptomatorCloudAccessCore +import CryptomatorCryptoLib +import Dependencies +import Foundation +import JOSESwift +import UIKit + +public enum HubAuthenticationViewModelError: Error { + case missingHubConfig + case missingAuthState + case missingSubscriptionHeader + case unexpectedSubscriptionHeader +} + +public protocol HubAuthenticationViewModelDelegate: AnyObject { + @MainActor + func hubAuthenticationViewModelWantsToShowLoadingIndicator() + + @MainActor + func hubAuthenticationViewModelWantsToHideLoadingIndicator() async + + @MainActor + func hubAuthenticationViewModelWantsToShowNeedsAccountInitAlert(profileURL: URL) +} + +public final class HubAuthenticationViewModel: ObservableObject { + public enum State: Equatable { + case accessNotGranted + case licenseExceeded + case deviceRegistration(DeviceRegistration) + case error(description: String) + } + + public enum DeviceRegistration: Equatable { + case deviceName + case needsAuthorization + } + + private enum Constants { + static var subscriptionState: String { "hub-subscription-state" } + } + + @Published var authenticationFlowState: State? + @Published public var deviceName: String = UIDevice.current.name + @Published public var setupCode: String = "" + private(set) var isLoggedIn = false + + private let vaultConfig: UnverifiedVaultConfig + private let authState: OIDAuthState + private let unlockHandler: HubVaultUnlockHandler + @Dependency(\.hubDeviceRegisteringService) var deviceRegisteringService + @Dependency(\.hubKeyService) var hubKeyService + @Dependency(\.cryptomatorHubKeyProvider) var cryptomatorHubKeyProvider + private weak var delegate: HubAuthenticationViewModelDelegate? + + public init(authState: OIDAuthState, + vaultConfig: UnverifiedVaultConfig, + unlockHandler: HubVaultUnlockHandler, + delegate: HubAuthenticationViewModelDelegate) { + self.authState = authState + self.vaultConfig = vaultConfig + self.unlockHandler = unlockHandler + self.delegate = delegate + } + + public func register() async { + guard let hubConfig = vaultConfig.allegedHubConfig else { + await setStateToErrorState(with: HubAuthenticationViewModelError.missingHubConfig) + return + } + + do { + try await deviceRegisteringService.registerDevice(withName: deviceName, hubConfig: hubConfig, authState: authState, setupCode: setupCode) + } catch { + await setStateToErrorState(with: error) + return + } + await setState(to: .deviceRegistration(.needsAuthorization)) + } + + public func refresh() async { + await continueToAccessCheck() + } + + public func continueToAccessCheck() async { + await delegate?.hubAuthenticationViewModelWantsToShowLoadingIndicator() + + let authFlow: HubAuthenticationFlow + do { + authFlow = try await hubKeyService.receiveKey(authState: authState, vaultConfig: vaultConfig) + } catch { + await setStateToErrorState(with: error) + return + } + await delegate?.hubAuthenticationViewModelWantsToHideLoadingIndicator() + + switch authFlow { + case let .success(response): + await receivedExistingKey(response) + case .accessNotGranted: + await setState(to: .accessNotGranted) + case .needsDeviceRegistration: + await setState(to: .deviceRegistration(.deviceName)) + case .licenseExceeded: + await setState(to: .licenseExceeded) + case let .requiresAccountInitialization(profileURL): + await delegate?.hubAuthenticationViewModelWantsToShowNeedsAccountInitAlert(profileURL: profileURL) + } + } + + private func receivedExistingKey(_ flowResponse: HubAuthenticationFlowSuccess) async { + let subscriptionState: HubSubscriptionState + let userKey: P384.KeyAgreement.PrivateKey + do { + let deviceKey = try cryptomatorHubKeyProvider.getPrivateKey() + userKey = try JWEHelper.decryptUserKey(jwe: flowResponse.encryptedUserKey, privateKey: deviceKey) + subscriptionState = try getSubscriptionState(from: flowResponse.header) + } catch { + await setStateToErrorState(with: error) + return + } + + let response = HubUnlockResponse(jwe: flowResponse.encryptedVaultKey, + privateKey: userKey, + subscriptionState: subscriptionState) + await MainActor.run { isLoggedIn = true } + await unlockHandler.didSuccessfullyRemoteUnlock(response) + } + + @MainActor + private func setState(to newState: State) { + authenticationFlowState = newState + } + + private func setStateToErrorState(with error: Error) async { + await delegate?.hubAuthenticationViewModelWantsToHideLoadingIndicator() + await setState(to: .error(description: error.localizedDescription)) + } + + private func getSubscriptionState(from header: [AnyHashable: Any]) throws -> HubSubscriptionState { + guard let subscriptionStateValue = header[Constants.subscriptionState] as? String else { + DDLogError("Can't retrieve hub subscription state from header -> missing value") + throw HubAuthenticationViewModelError.missingSubscriptionHeader + } + switch subscriptionStateValue { + case "ACTIVE": + return .active + case "INACTIVE": + return .inactive + default: + DDLogError("Can't retrieve hub subscription state from header -> unexpected value") + throw HubAuthenticationViewModelError.unexpectedSubscriptionHeader + } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift new file mode 100644 index 000000000..c0328584f --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct HubAccessNotGrantedView: View { + var onRefresh: () -> Void + + var body: some View { + CryptomatorSimpleButtonView( + buttonTitle: LocalizedString.getValue("common.button.refresh"), + onButtonTap: onRefresh, + headerTitle: LocalizedString.getValue("hubAuthentication.accessNotGranted") + ) + } +} + +struct HubDeviceRegisteredSuccessfullyView_Previews: PreviewProvider { + static var previews: some View { + HubAccessNotGrantedView(onRefresh: {}) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteringService.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteringService.swift new file mode 100644 index 000000000..bf1de0b72 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteringService.swift @@ -0,0 +1,67 @@ +// +// HubDeviceRegisteringService.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 19.11.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import AppAuthCore +import CryptomatorCloudAccessCore +import Dependencies +import Foundation +import XCTestDynamicOverlay + +public protocol HubDeviceRegistering { + func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String) async throws +} + +private enum HubDeviceRegisteringKey: DependencyKey { + static var liveValue: HubDeviceRegistering = CryptomatorHubAuthenticator() + #if DEBUG + static var testValue: HubDeviceRegistering = UnimplementedHubDeviceRegisteringService() + #endif +} + +extension DependencyValues { + var hubDeviceRegisteringService: HubDeviceRegistering { + get { self[HubDeviceRegisteringKey.self] } + set { self[HubDeviceRegisteringKey.self] = newValue } + } +} + +#if DEBUG +final class UnimplementedHubDeviceRegisteringService: HubDeviceRegistering { + func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String) async throws { + XCTFail("\(Self.self).registerDevice is unimplemented.") + } +} + +// MARK: - HubDeviceRegisteringMock - + +// swiftlint: disable all +final class HubDeviceRegisteringMock: HubDeviceRegistering { + // MARK: - registerDevice + + var registerDeviceWithNameHubConfigAuthStateSetupCodeThrowableError: Error? + var registerDeviceWithNameHubConfigAuthStateSetupCodeCallsCount = 0 + var registerDeviceWithNameHubConfigAuthStateSetupCodeCalled: Bool { + registerDeviceWithNameHubConfigAuthStateSetupCodeCallsCount > 0 + } + + var registerDeviceWithNameHubConfigAuthStateSetupCodeReceivedArguments: (name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String)? + var registerDeviceWithNameHubConfigAuthStateSetupCodeReceivedInvocations: [(name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String)] = [] + var registerDeviceWithNameHubConfigAuthStateSetupCodeClosure: ((String, HubConfig, OIDAuthState, String) throws -> Void)? + + func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String) throws { + if let error = registerDeviceWithNameHubConfigAuthStateSetupCodeThrowableError { + throw error + } + registerDeviceWithNameHubConfigAuthStateSetupCodeCallsCount += 1 + registerDeviceWithNameHubConfigAuthStateSetupCodeReceivedArguments = (name: name, hubConfig: hubConfig, authState: authState, setupCode: setupCode) + registerDeviceWithNameHubConfigAuthStateSetupCodeReceivedInvocations.append((name: name, hubConfig: hubConfig, authState: authState, setupCode: setupCode)) + try registerDeviceWithNameHubConfigAuthStateSetupCodeClosure?(name, hubConfig, authState, setupCode) + } +} +// swiftlint: enable all +#endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift new file mode 100644 index 000000000..92bf6ce50 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift @@ -0,0 +1,56 @@ +import SwiftUI + +struct HubDeviceRegistrationView: View { + @Binding var deviceName: String + @Binding var accountKey: String + var onRegisterTap: () -> Void + + @FocusStateLegacy private var field: Field? = .deviceName + + private enum Field: CaseIterable { + case deviceName + case accountKey + } + + var body: some View { + List { + Section { + TextField( + LocalizedString.getValue("hubAuthentication.deviceRegistration.deviceName.cells.name"), + text: $deviceName, + onCommit: { field = .accountKey } + ) + .focusedLegacy($field, equals: .deviceName) + .backportedSubmitlabel(.next) + } footer: { + Text(LocalizedString.getValue("hubAuthentication.deviceRegistration.deviceName.footer.title")) + } + + Section { + TextField( + "Account Key", + text: $accountKey, + onCommit: onRegisterTap + ) + .focusedLegacy($field, equals: .accountKey) + .backportedSubmitlabel(.done) + } footer: { + Text(LocalizedString.getValue("hubAuthentication.deviceRegistration.accountKey.footer.title")) + } + } + .setListBackgroundColor(.cryptomatorBackground) + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button(LocalizedString.getValue("common.button.register")) { + onRegisterTap() + } + } + } + } +} + +struct HubDeviceRegistrationView_Previews: PreviewProvider { + static var previews: some View { + HubDeviceRegistrationView(deviceName: .constant(""), accountKey: .constant(""), onRegisterTap: {}) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubKeyService.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubKeyService.swift new file mode 100644 index 000000000..d156d04fb --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubKeyService.swift @@ -0,0 +1,65 @@ +// +// HubKeyService.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 19.11.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import AppAuthCore +import CryptomatorCloudAccessCore +import Dependencies +import Foundation + +public protocol HubKeyReceiving { + func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow +} + +private enum HubKeyReceivingDependencyKey: DependencyKey { + static let liveValue: HubKeyReceiving = CryptomatorHubAuthenticator() + #if DEBUG + static let testValue: HubKeyReceiving = UnimplementedHubKeyReceivingService() + #endif +} + +extension DependencyValues { + var hubKeyService: HubKeyReceiving { + get { self[HubKeyReceivingDependencyKey.self] } + set { self[HubKeyReceivingDependencyKey.self] = newValue } + } +} + +#if DEBUG +final class UnimplementedHubKeyReceivingService: HubKeyReceiving { + func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow { + unimplemented(placeholder: .accessNotGranted) + } +} + +// MARK: - HubKeyReceivingMock - + +final class HubKeyReceivingMock: HubKeyReceiving { + // MARK: - receiveKey + + var receiveKeyAuthStateVaultConfigThrowableError: Error? + var receiveKeyAuthStateVaultConfigCallsCount = 0 + var receiveKeyAuthStateVaultConfigCalled: Bool { + receiveKeyAuthStateVaultConfigCallsCount > 0 + } + + var receiveKeyAuthStateVaultConfigReceivedArguments: (authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig)? + var receiveKeyAuthStateVaultConfigReceivedInvocations: [(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig)] = [] + var receiveKeyAuthStateVaultConfigReturnValue: HubAuthenticationFlow! + var receiveKeyAuthStateVaultConfigClosure: ((OIDAuthState, UnverifiedVaultConfig) throws -> HubAuthenticationFlow)? + + func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) throws -> HubAuthenticationFlow { + if let error = receiveKeyAuthStateVaultConfigThrowableError { + throw error + } + receiveKeyAuthStateVaultConfigCallsCount += 1 + receiveKeyAuthStateVaultConfigReceivedArguments = (authState: authState, vaultConfig: vaultConfig) + receiveKeyAuthStateVaultConfigReceivedInvocations.append((authState: authState, vaultConfig: vaultConfig)) + return try receiveKeyAuthStateVaultConfigClosure.map({ try $0(authState, vaultConfig) }) ?? receiveKeyAuthStateVaultConfigReturnValue + } +} +#endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubLoginView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubLoginView.swift new file mode 100644 index 000000000..86dffcd3b --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubLoginView.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct HubLoginView: View { + var onLogin: () -> Void + + var body: some View { + CryptomatorSimpleButtonView( + buttonTitle: "Login", + onButtonTap: onLogin, + headerTitle: "Login to unlock your vault" + ) + } +} + +struct HubLoginView_Previews: PreviewProvider { + static var previews: some View { + HubLoginView(onLogin: {}) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubRepository.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubRepository.swift new file mode 100644 index 000000000..f44ee2488 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubRepository.swift @@ -0,0 +1,72 @@ +import Dependencies +import Foundation +import GRDB + +public protocol HubRepository { + func save(_ vault: HubVault) throws + func getHubVault(vaultID: String) throws -> HubVault? +} + +public struct HubVault: Equatable { + public let vaultUID: String + public let subscriptionState: HubSubscriptionState +} + +private struct HubVaultRow: Codable, Equatable, PersistableRecord, FetchableRecord { + public static let databaseTableName = "hubVaultAccount" + + let vaultUID: String + let subscriptionState: HubSubscriptionState + + init(from vault: HubVault) { + self.vaultUID = vault.vaultUID + self.subscriptionState = vault.subscriptionState + } + + func toHubVault() -> HubVault { + HubVault(vaultUID: vaultUID, subscriptionState: subscriptionState) + } + + enum Columns: String, ColumnExpression { + case vaultUID, subscriptionState + } + + public func encode(to container: inout PersistenceContainer) { + container[Columns.vaultUID] = vaultUID + container[Columns.subscriptionState] = subscriptionState + } +} + +extension HubSubscriptionState: DatabaseValueConvertible {} + +public extension DependencyValues { + var hubRepository: HubRepository { + get { self[HubRepositoryKey.self] } + set { self[HubRepositoryKey.self] = newValue } + } +} + +private enum HubRepositoryKey: DependencyKey { + static var liveValue: HubRepository = HubDBRepository() + #if DEBUG + static var testValue: HubRepository = HubRepositoryMock() + #endif +} + +public class HubDBRepository: HubRepository { + @Dependency(\.database) private var database + + public func save(_ vault: HubVault) throws { + let row = HubVaultRow(from: vault) + try database.write { db in + try row.save(db) + } + } + + public func getHubVault(vaultID: String) throws -> HubVault? { + let row = try database.read { db in + try HubVaultRow.fetchOne(db, key: vaultID) + } + return row?.toHubVault() + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubSubscriptionState.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubSubscriptionState.swift new file mode 100644 index 000000000..daf4d3185 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubSubscriptionState.swift @@ -0,0 +1,4 @@ +public enum HubSubscriptionState: String, Codable { + case active + case inactive +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubUserAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubUserAuthenticator.swift new file mode 100644 index 000000000..d8f144599 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubUserAuthenticator.swift @@ -0,0 +1,17 @@ +import AppAuthCore +import CryptomatorCloudAccessCore +import UIKit + +struct HubUserAuthenticator: HubUserLogin { + private let hubAuthenticator: HubAuthenticating + private let viewController: UIViewController + + init(hubAuthenticator: HubAuthenticating, viewController: UIViewController) { + self.hubAuthenticator = hubAuthenticator + self.viewController = viewController + } + + func authenticate(with hubConfig: HubConfig) async throws -> OIDAuthState { + try await hubAuthenticator.authenticate(with: hubConfig, from: viewController) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubUserLogin.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubUserLogin.swift new file mode 100644 index 000000000..219bae4b1 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubUserLogin.swift @@ -0,0 +1,7 @@ +import AppAuthCore +import CryptomatorCloudAccessCore +import Foundation + +public protocol HubUserLogin { + func authenticate(with hubConfig: HubConfig) async throws -> OIDAuthState +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift new file mode 100644 index 000000000..3f58f2a85 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift @@ -0,0 +1,70 @@ +import AppAuthCore +import CryptoKit +import CryptomatorCloudAccessCore +import CryptomatorCryptoLib +import Dependencies +import JOSESwift +import SwiftUI +import UIKit + +public final class HubXPCLoginCoordinator: Coordinator { + public var childCoordinators = [Coordinator]() + public var navigationController: UINavigationController + let domain: NSFileProviderDomain + let vaultConfig: UnverifiedVaultConfig + public let onUnlocked: () -> Void + public let onErrorAlertDismissed: () -> Void + @Dependency(\.hubRepository) private var hubRepository + @Dependency(\.fileProviderConnector) private var fileProviderConnector + + public init(navigationController: UINavigationController, + domain: NSFileProviderDomain, + vaultConfig: UnverifiedVaultConfig, + onUnlocked: @escaping () -> Void, + onErrorAlertDismissed: @escaping () -> Void) { + self.navigationController = navigationController + self.domain = domain + self.vaultConfig = vaultConfig + self.onUnlocked = onUnlocked + self.onErrorAlertDismissed = onErrorAlertDismissed + } + + public func start() { + let unlockHandler = HubXPCVaultUnlockHandler(fileProviderConnector: fileProviderConnector, domain: domain, delegate: self) + prepareNavigationControllerForLogin() + let child = HubAuthenticationCoordinator(navigationController: navigationController, + vaultConfig: vaultConfig, + unlockHandler: unlockHandler, + parent: self, + delegate: self) + childCoordinators.append(child) + child.start() + } + + /// Prepares the `UINavigationController` for the hub authentication flow. + /// + /// As the FileProviderExtensionUI is always shown as a sheet and the login is initially just a alert which asks the user to open a website, we want to hide the navigation bar initially. + private func prepareNavigationControllerForLogin() { + navigationController.setNavigationBarHidden(true, animated: false) + } +} + +extension HubXPCLoginCoordinator: HubVaultUnlockHandlerDelegate { + public func successfullyProcessedUnlockedVault() { + onUnlocked() + } + + public func failedToProcessUnlockedVault(error: Error) { + handleError(error, for: navigationController, onOKTapped: onErrorAlertDismissed) + } +} + +extension HubXPCLoginCoordinator: HubAuthenticationCoordinatorDelegate { + public func userDidCancelHubAuthentication() { + onErrorAlertDismissed() + } + + public func userDismissedHubAuthenticationErrorMessage() { + onErrorAlertDismissed() + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/AddHubVaultUnlockHandler.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/AddHubVaultUnlockHandler.swift new file mode 100644 index 000000000..8be0234a6 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/AddHubVaultUnlockHandler.swift @@ -0,0 +1,41 @@ +import Foundation + +public struct AddHubVaultUnlockHandler: HubVaultUnlockHandler { + private let vaultUID: String + private let accountUID: String + private let vaultItem: VaultItem + private let downloadedVaultConfig: DownloadedVaultConfig + private let vaultManager: VaultManager + private weak var delegate: HubVaultUnlockHandlerDelegate? + + public init(vaultUID: String, + accountUID: String, + vaultItem: VaultItem, + downloadedVaultConfig: DownloadedVaultConfig, + vaultManager: VaultManager, + delegate: HubVaultUnlockHandlerDelegate?) { + self.vaultUID = vaultUID + self.accountUID = accountUID + self.vaultItem = vaultItem + self.downloadedVaultConfig = downloadedVaultConfig + self.vaultManager = vaultManager + self.delegate = delegate + } + + public func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async { + let jwe = response.jwe + let privateKey = response.privateKey + let hubVault = ExistingHubVault(vaultUID: vaultUID, + delegateAccountUID: accountUID, + jweData: jwe.compactSerializedData, + privateKey: privateKey, + vaultItem: vaultItem, + downloadedVaultConfig: downloadedVaultConfig) + do { + try await vaultManager.addExistingHubVault(hubVault).getValue() + await delegate?.successfullyProcessedUnlockedVault() + } catch { + await delegate?.failedToProcessUnlockedVault(error: error) + } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubVaultUnlockHandler.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubVaultUnlockHandler.swift new file mode 100644 index 000000000..b99e7fc77 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubVaultUnlockHandler.swift @@ -0,0 +1,38 @@ +import Foundation + +public protocol HubVaultUnlockHandler { + func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async +} + +public protocol HubVaultUnlockHandlerDelegate: AnyObject { + @MainActor + func successfullyProcessedUnlockedVault() + @MainActor + func failedToProcessUnlockedVault(error: Error) +} + +// MARK: - HubVaultUnlockHandlerMock - + +#if DEBUG +// swiftlint: disable all +final class HubVaultUnlockHandlerMock: HubVaultUnlockHandler { + // MARK: - didSuccessfullyRemoteUnlock + + var didSuccessfullyRemoteUnlockCallsCount = 0 + var didSuccessfullyRemoteUnlockCalled: Bool { + didSuccessfullyRemoteUnlockCallsCount > 0 + } + + var didSuccessfullyRemoteUnlockReceivedResponse: HubUnlockResponse? + var didSuccessfullyRemoteUnlockReceivedInvocations: [HubUnlockResponse] = [] + var didSuccessfullyRemoteUnlockClosure: ((HubUnlockResponse) -> Void)? + + func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) { + didSuccessfullyRemoteUnlockCallsCount += 1 + didSuccessfullyRemoteUnlockReceivedResponse = response + didSuccessfullyRemoteUnlockReceivedInvocations.append(response) + didSuccessfullyRemoteUnlockClosure?(response) + } +} +// / swiftlint: enable all +#endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubXPCVaultUnlockHandler.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubXPCVaultUnlockHandler.swift new file mode 100644 index 000000000..d52575fdc --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubXPCVaultUnlockHandler.swift @@ -0,0 +1,41 @@ +import CryptomatorCryptoLib +import Dependencies +import FileProvider + +struct HubXPCVaultUnlockHandler: HubVaultUnlockHandler { + private let fileProviderConnector: FileProviderConnector + private let domain: NSFileProviderDomain + private weak var delegate: HubVaultUnlockHandlerDelegate? + @Dependency(\.hubRepository) private var hubRepository + + init(fileProviderConnector: FileProviderConnector, + domain: NSFileProviderDomain, + delegate: HubVaultUnlockHandlerDelegate) { + self.fileProviderConnector = fileProviderConnector + self.domain = domain + self.delegate = delegate + } + + func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async { + let masterkey: Masterkey + do { + masterkey = try JWEHelper.decryptVaultKey(jwe: response.jwe, with: response.privateKey) + } catch { + await delegate?.failedToProcessUnlockedVault(error: error) + return + } + do { + let xpc: XPC = try await fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain) + defer { + fileProviderConnector.invalidateXPC(xpc) + } + try await xpc.proxy.unlockVault(rawKey: masterkey.rawKey).getValue() + let hubVault = HubVault(vaultUID: domain.identifier.rawValue, subscriptionState: response.subscriptionState) + try hubRepository.save(hubVault) + await delegate?.successfullyProcessedUnlockedVault() + } catch { + await delegate?.failedToProcessUnlockedVault(error: error) + return + } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift new file mode 100644 index 000000000..62b8a1037 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift @@ -0,0 +1,103 @@ +// +// JWEHelper.swift +// +// +// Created by Philipp Schmid on 22.07.22. +// + +import CryptoKit +import CryptomatorCryptoLib +import Foundation +import JOSESwift +import SwiftECC + +public enum JWEHelperError: Error { + case invalidDecrypter + case invalidMasterkeyPayload +} + +public enum JWEHelper { + public static func decryptVaultKey(jwe: JWE, with privateKey: P384.KeyAgreement.PrivateKey) throws -> Masterkey { + // see https://developer.apple.com/forums/thread/680554 + let x = privateKey.x963Representation[1 ..< 49] + let y = privateKey.x963Representation[49 ..< 97] + let k = privateKey.x963Representation[97 ..< 145] + let decryptionKey = try ECPrivateKey(crv: "P-384", x: x.base64UrlEncodedString(), y: y.base64UrlEncodedString(), privateKey: k.base64UrlEncodedString()) + + guard let decrypter = Decrypter(keyManagementAlgorithm: .ECDH_ES, contentEncryptionAlgorithm: .A256GCM, decryptionKey: decryptionKey) else { + throw JWEHelperError.invalidDecrypter + } + let payload = try jwe.decrypt(using: decrypter) + let payloadMasterkey = try JSONDecoder().decode(PayloadMasterkey.self, from: payload.data()) + + guard let masterkeyData = Data(base64Encoded: payloadMasterkey.key) else { + throw JWEHelperError.invalidMasterkeyPayload + } + return Masterkey.createFromRaw(rawKey: [UInt8](masterkeyData)) + } + + public static func decryptUserKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey) throws -> P384.KeyAgreement.PrivateKey { + let x = privateKey.x963Representation[1 ..< 49] + let y = privateKey.x963Representation[49 ..< 97] + let k = privateKey.x963Representation[97 ..< 145] + let decryptionKey = try ECPrivateKey(crv: "P-384", + x: x.base64UrlEncodedString(), + y: y.base64UrlEncodedString(), + privateKey: k.base64UrlEncodedString()) + guard let decrypter = Decrypter(keyManagementAlgorithm: .ECDH_ES, + contentEncryptionAlgorithm: .A256GCM, + decryptionKey: decryptionKey) else { + throw JWEHelperError.invalidDecrypter + } + let payload = try jwe.decrypt(using: decrypter) + return try decodeUserKey(payload: payload) + } + + public static func decryptUserKey(jwe: JWE, setupCode: String) throws -> P384.KeyAgreement.PrivateKey { + guard let decrypter = Decrypter(keyManagementAlgorithm: .PBES2_HS512_A256KW, + contentEncryptionAlgorithm: .A256GCM, + decryptionKey: setupCode) else { + throw JWEHelperError.invalidDecrypter + } + let payload = try jwe.decrypt(using: decrypter) + return try decodeUserKey(payload: payload) + } + + public static func encryptUserKey(userKey: P384.KeyAgreement.PrivateKey, deviceKey: P384.KeyAgreement.PublicKey) throws -> JWE { + let header = JWEHeader(keyManagementAlgorithm: .ECDH_ES, contentEncryptionAlgorithm: .A256GCM) + let x = deviceKey.x963Representation[1 ..< 49] + let y = deviceKey.x963Representation[49 ..< 97] + let encryptionKey = ECPublicKey(crv: .P384, + x: x.base64EncodedString(), + y: y.base64EncodedString()) + guard let encrypter = Encrypter(keyManagementAlgorithm: .ECDH_ES, + contentEncryptionAlgorithm: .A256GCM, + encryptionKey: encryptionKey) else { + throw JWEHelperError.invalidDecrypter + } + let payloadKey = try PayloadMasterkey(key: userKey.derPkcs8().base64EncodedString()) + let payload = try Payload(JSONEncoder().encode(payloadKey)) + return try JWE(header: header, payload: payload, encrypter: encrypter) + } + + private static func decodeUserKey(payload: Payload) throws -> P384.KeyAgreement.PrivateKey { + let decodedPayload = try JSONDecoder().decode(PayloadMasterkey.self, from: payload.data()) + + guard let privateKeyData = Data(base64Encoded: decodedPayload.key) else { + throw JWEHelperError.invalidMasterkeyPayload + } + return try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: privateKeyData) + } +} + +public extension P384.KeyAgreement.PrivateKey { + init(pkcs8DerRepresentation: Data) throws { + let privateKey = try ECPrivateKey(der: Array(pkcs8DerRepresentation), pkcs8: true) + try self.init(pemRepresentation: privateKey.pem) + } + + func derPkcs8() throws -> Data { + let privateKey = try ECPrivateKey(pem: pemRepresentation) + return Data(privateKey.derPkcs8) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderAccountDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderAccountDBManager.swift index 2e460699a..5fe7ded1f 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderAccountDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderAccountDBManager.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Skymatic GmbH. All rights reserved. // +import Dependencies import Foundation import GRDB @@ -41,15 +42,11 @@ public protocol CloudProviderAccountManager { } public class CloudProviderAccountDBManager: CloudProviderAccountManager { - public static let shared = CloudProviderAccountDBManager(dbPool: CryptomatorDatabase.shared.dbPool) - private let dbPool: DatabasePool - - init(dbPool: DatabasePool) { - self.dbPool = dbPool - } + @Dependency(\.database) var database + public static let shared = CloudProviderAccountDBManager() public func getCloudProviderType(for accountUID: String) throws -> CloudProviderType { - let cloudAccount = try dbPool.read { db in + let cloudAccount = try database.read { db in return try CloudProviderAccount.fetchOne(db, key: accountUID) } guard let providerType = cloudAccount?.cloudProviderType else { @@ -59,7 +56,7 @@ public class CloudProviderAccountDBManager: CloudProviderAccountManager { } public func getAllAccountUIDs(for type: CloudProviderType) throws -> [String] { - let accounts: [CloudProviderAccount] = try dbPool.read { db in + let accounts: [CloudProviderAccount] = try database.read { db in return try CloudProviderAccount .filter(Column("cloudProviderType") == type) .fetchAll(db) @@ -68,13 +65,13 @@ public class CloudProviderAccountDBManager: CloudProviderAccountManager { } public func saveNewAccount(_ account: CloudProviderAccount) throws { - try dbPool.write { db in + try database.write { db in try account.save(db) } } public func removeAccount(with accountUID: String) throws { - try dbPool.write { db in + try database.write { db in guard try CloudProviderAccount.deleteOne(db, key: accountUID) else { throw CloudProviderAccountError.accountNotFoundError } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/ExistingHubVault.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/ExistingHubVault.swift new file mode 100644 index 000000000..6851589c3 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/ExistingHubVault.swift @@ -0,0 +1,20 @@ +import CryptoKit +import Foundation + +public struct ExistingHubVault { + let vaultUID: String + let delegateAccountUID: String + let jweData: Data + let privateKey: P384.KeyAgreement.PrivateKey + let vaultItem: VaultItem + let downloadedVaultConfig: DownloadedVaultConfig + + public init(vaultUID: String, delegateAccountUID: String, jweData: Data, privateKey: P384.KeyAgreement.PrivateKey, vaultItem: VaultItem, downloadedVaultConfig: DownloadedVaultConfig) { + self.vaultUID = vaultUID + self.delegateAccountUID = delegateAccountUID + self.jweData = jweData + self.privateKey = privateKey + self.vaultItem = vaultItem + self.downloadedVaultConfig = downloadedVaultConfig + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultAccountDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultAccountDBManager.swift index 018d49060..3069ca55b 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultAccountDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultAccountDBManager.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import Dependencies import Foundation import GRDB @@ -55,16 +56,12 @@ public enum VaultAccountManagerError: Error { } public class VaultAccountDBManager: VaultAccountManager { - public static let shared = VaultAccountDBManager(dbPool: CryptomatorDatabase.shared.dbPool) - private let dbPool: DatabasePool - - public init(dbPool: DatabasePool) { - self.dbPool = dbPool - } + public static let shared = VaultAccountDBManager() + @Dependency(\.database) private var database public func saveNewAccount(_ account: VaultAccount) throws { do { - try dbPool.write { db in + try database.write { db in try account.save(db) } } catch let error as DatabaseError where error.resultCode == .SQLITE_CONSTRAINT { @@ -73,7 +70,7 @@ public class VaultAccountDBManager: VaultAccountManager { } public func removeAccount(with vaultUID: String) throws { - try dbPool.write { db in + try database.write { db in guard try VaultAccount.deleteOne(db, key: vaultUID) else { throw CloudProviderAccountError.accountNotFoundError } @@ -81,7 +78,7 @@ public class VaultAccountDBManager: VaultAccountManager { } public func getAccount(with vaultUID: String) throws -> VaultAccount { - let fetchedAccount = try dbPool.read { db in + let fetchedAccount = try database.read { db in return try VaultAccount.fetchOne(db, key: vaultUID) } guard let account = fetchedAccount else { @@ -91,13 +88,13 @@ public class VaultAccountDBManager: VaultAccountManager { } public func getAllAccounts() throws -> [VaultAccount] { - try dbPool.read { db in + try database.read { db in try VaultAccount.fetchAll(db) } } public func updateAccount(_ account: VaultAccount) throws { - try dbPool.write { db in + try database.write { db in try account.update(db) } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBCache.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBCache.swift index 52db65392..5c81ae1b5 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBCache.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBCache.swift @@ -8,6 +8,7 @@ import CryptomatorCloudAccessCore import CryptomatorCryptoLib +import Dependencies import Foundation import GRDB import Promises @@ -22,7 +23,7 @@ public protocol VaultCache { public struct CachedVault: Codable, Equatable { let vaultUID: String public let masterkeyFileData: Data - let vaultConfigToken: Data? + public let vaultConfigToken: Data? let lastUpToDateCheck: Date var masterkeyFileLastModifiedDate: Date? var vaultConfigLastModifiedDate: Date? @@ -49,20 +50,18 @@ public enum VaultCacheError: Error { } public class VaultDBCache: VaultCache { - private let dbWriter: DatabaseWriter + @Dependency(\.database) var database - public init(dbWriter: DatabaseWriter) { - self.dbWriter = dbWriter - } + public init() {} public func cache(_ entry: CachedVault) throws { - try dbWriter.write({ db in + try database.write({ db in try entry.save(db) }) } public func getCachedVault(withVaultUID vaultUID: String) throws -> CachedVault { - try dbWriter.read({ db in + try database.read({ db in guard let cachedVault = try CachedVault.fetchOne(db, key: vaultUID) else { throw VaultCacheError.vaultNotFound } @@ -83,7 +82,7 @@ public class VaultDBCache: VaultCache { } public func setMasterkeyFileData(_ data: Data, forVaultUID vaultUID: String, lastModifiedDate: Date?) throws { - _ = try dbWriter.write { db in + _ = try database.write { db in try CachedVault.filter(CachedVault.Columns.vaultUID == vaultUID).updateAll(db, CachedVault.Columns.masterkeyFileData.set(to: data), CachedVault.Columns.masterkeyFileLastModifiedDate.set(to: lastModifiedDate)) @@ -153,7 +152,7 @@ public class VaultDBCache: VaultCache { } private func setVaultConfigData(_ data: Data?, forVaultUID vaultUID: String, lastModifiedDate: Date?) throws { - _ = try dbWriter.write { db in + _ = try database.write { db in try CachedVault.filter(CachedVault.Columns.vaultUID == vaultUID).updateAll(db, CachedVault.Columns.vaultConfigToken.set(to: data), CachedVault.Columns.vaultConfigLastModifiedDate.set(to: lastModifiedDate)) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift index 3c00887dd..1c24c9aba 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift @@ -7,10 +7,12 @@ // import CocoaLumberjackSwift +import CryptoKit import CryptomatorCloudAccessCore import CryptomatorCryptoLib import FileProvider import Foundation +import JOSESwift import os.log import Promises @@ -19,6 +21,7 @@ public enum VaultManagerError: Error { case vaultVersionNotSupported case fileProviderDomainNotFound case moveVaultInsideItself + case missingVaultConfigToken } public protocol VaultManager { @@ -31,12 +34,14 @@ public protocol VaultManager { func removeAllUnusedFileProviderDomains() -> Promise func moveVault(account: VaultAccount, to targetVaultPath: CloudPath) -> Promise func changePassphrase(oldPassphrase: String, newPassphrase: String, forVaultUID vaultUID: String) -> Promise + func addExistingHubVault(_ vault: ExistingHubVault) -> Promise + func manualUnlockVault(withUID vaultUID: String, rawKey: [UInt8]) throws -> CloudProvider } public class VaultDBManager: VaultManager { public static let shared = VaultDBManager(providerManager: CloudProviderDBManager.shared, vaultAccountManager: VaultAccountDBManager.shared, - vaultCache: VaultDBCache(dbWriter: CryptomatorDatabase.shared.dbPool), + vaultCache: VaultDBCache(), passwordManager: VaultPasswordKeychainManager(), masterkeyCacheManager: MasterkeyCacheKeychainManager.shared, masterkeyCacheHelper: VaultKeepUnlockedManager.shared) @@ -74,7 +79,7 @@ public class VaultDBManager: VaultManager { */ public func createNewVault(withVaultUID vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise { let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(UUID().uuidString, isDirectory: true) - let cipherCombo = CryptorScheme.sivCtrMac + let cipherCombo = CryptorScheme.sivGcm let vaultConfig = VaultConfig.createNew(format: 8, cipherCombo: cipherCombo, shorteningThreshold: 220) let masterkey: Masterkey let provider: LocalizedCloudProviderDecorator @@ -219,6 +224,122 @@ public class VaultDBManager: VaultManager { } } + // swiftlint:disable:next function_parameter_count + public func createFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, downloadedVaultConfig: DownloadedVaultConfig, downloadedMasterkey: DownloadedMasterkeyFile, vaultItem: VaultItem, password: String) -> Promise { + let provider: LocalizedCloudProviderDecorator + do { + provider = try LocalizedCloudProviderDecorator(delegate: providerManager.getProvider(with: delegateAccountUID)) + } catch { + return Promise(error) + } + let vaultPath = vaultItem.vaultPath + let vaultConfigMetadata = downloadedVaultConfig.metadata + let vaultConfigToken = downloadedVaultConfig.token + let masterkeyFile = downloadedMasterkey.masterkeyFile + let masterkeyFileData = downloadedMasterkey.masterkeyFileData + let masterkeyFileMetadata = downloadedMasterkey.metadata + do { + let masterkey = try masterkeyFile.unlock(passphrase: password) + _ = try VaultProviderFactory.createVaultProvider(from: downloadedVaultConfig.vaultConfig, masterkey: masterkey, vaultPath: vaultPath, with: provider.delegate) + } catch { + return Promise(error) + } + let vaultConfigLastModifiedDate = vaultConfigMetadata.lastModifiedDate + let masterkeyFileLastModifiedDate = masterkeyFileMetadata.lastModifiedDate + let lastUpToDateCheck: Date = (vaultConfigLastModifiedDate ?? .distantPast) < (masterkeyFileLastModifiedDate ?? .distantPast) ? masterkeyFileLastModifiedDate! : vaultConfigLastModifiedDate ?? Date() + let cachedVault = CachedVault(vaultUID: vaultUID, masterkeyFileData: masterkeyFileData, vaultConfigToken: vaultConfigToken, lastUpToDateCheck: lastUpToDateCheck, masterkeyFileLastModifiedDate: masterkeyFileMetadata.lastModifiedDate, vaultConfigLastModifiedDate: vaultConfigMetadata.lastModifiedDate) + return addFileProviderDomain(forVaultUID: vaultUID, displayName: vaultItem.name).then { + let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: vaultItem.name) + try self.vaultAccountManager.saveNewAccount(vaultAccount) + try self.postProcessVaultCreation(cachedVault: cachedVault, password: password, storePasswordInKeychain: false) + DDLogInfo("Opened existing vault \"\(vaultItem.name)\" (\(vaultUID))") + } + } + + public func getUnverifiedVaultConfig(delegateAccountUID: String, vaultItem: VaultItem) -> Promise { + let provider: LocalizedCloudProviderDecorator + do { + provider = try LocalizedCloudProviderDecorator(delegate: providerManager.getProvider(with: delegateAccountUID)) + } catch { + return Promise(error) + } + let tmpDirURL = FileManager.default.temporaryDirectory + let localVaultConfigURL = tmpDirURL.appendingPathComponent(UUID().uuidString, isDirectory: false) + let vaultPath = vaultItem.vaultPath + let vaultConfigPath = vaultPath.appendingPathComponent("vault.cryptomator") + return provider.downloadFileWithMetadata(from: vaultConfigPath, to: localVaultConfigURL).then { vaultConfigMetadata -> DownloadedVaultConfig in + let vaultConfigToken = try Data(contentsOf: localVaultConfigURL) + let unverifiedVaultConfig = try UnverifiedVaultConfig(token: vaultConfigToken) + return DownloadedVaultConfig(vaultConfig: unverifiedVaultConfig, token: vaultConfigToken, metadata: vaultConfigMetadata) + } + } + + public func downloadMasterkeyFile(delegateAccountUID: String, vaultItem: VaultItem) -> Promise { + let provider: LocalizedCloudProviderDecorator + do { + provider = try LocalizedCloudProviderDecorator(delegate: providerManager.getProvider(with: delegateAccountUID)) + } catch { + return Promise(error) + } + let tmpDirURL = FileManager.default.temporaryDirectory + let localMasterkeyURL = tmpDirURL.appendingPathComponent(UUID().uuidString, isDirectory: false) + let vaultPath = vaultItem.vaultPath + let masterkeyPath = vaultPath.appendingPathComponent("masterkey.cryptomator") + return provider.downloadFileWithMetadata(from: masterkeyPath, to: localMasterkeyURL).then { masterkeyFileMetadata -> DownloadedMasterkeyFile in + let masterkeyFileData = try Data(contentsOf: localMasterkeyURL) + let masterkeyFile = try MasterkeyFile.withContentFromData(data: masterkeyFileData) + return DownloadedMasterkeyFile(masterkeyFile: masterkeyFile, metadata: masterkeyFileMetadata, masterkeyFileData: masterkeyFileData) + } + } + + public func addExistingHubVault(_ vault: ExistingHubVault) -> Promise { + let delegateAccountUID = vault.delegateAccountUID + let provider: LocalizedCloudProviderDecorator + do { + provider = try LocalizedCloudProviderDecorator(delegate: providerManager.getProvider(with: delegateAccountUID)) + } catch { + return Promise(error) + } + let vaultItem = vault.vaultItem + let downloadedVaultConfig = vault.downloadedVaultConfig + let jweData = vault.jweData + + let vaultPath = vaultItem.vaultPath + let vaultConfigMetadata = downloadedVaultConfig.metadata + let vaultConfigToken = downloadedVaultConfig.token + let masterkey: Masterkey + do { + let jwe = try JWE(compactSerialization: jweData) + masterkey = try JWEHelper.decryptVaultKey(jwe: jwe, with: vault.privateKey) + } catch { + return Promise(error) + } + do { + _ = try VaultProviderFactory.createVaultProvider(from: downloadedVaultConfig.vaultConfig, masterkey: masterkey, vaultPath: vaultPath, with: provider.delegate) + } catch { + return Promise(error) + } + let vaultUID = vault.vaultUID + let cachedVault = CachedVault(vaultUID: vaultUID, + masterkeyFileData: jweData, + vaultConfigToken: vaultConfigToken, + lastUpToDateCheck: Date(), + masterkeyFileLastModifiedDate: nil, + vaultConfigLastModifiedDate: vaultConfigMetadata.lastModifiedDate) + return addFileProviderDomain(forVaultUID: vaultUID, displayName: vaultItem.name).then { + let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: vaultItem.name) + try self.vaultAccountManager.saveNewAccount(vaultAccount) + do { + try self.postProcessVaultCreation(cachedVault: cachedVault, password: nil) + } catch { + try self.vaultAccountManager.removeAccount(with: vaultUID) + _ = self.removeFileProviderDomain(withVaultUID: vaultUID) + throw error + } + DDLogInfo("Opened existing vault \"\(vaultItem.name)\" (\(vaultUID))") + } + } + /** Imports an existing legacy Vault. @@ -333,6 +454,26 @@ public class VaultDBManager: VaultManager { return try createVaultProvider(cachedVault: cachedVault, masterkey: masterkey, masterkeyFile: masterkeyFile) } + public func manualUnlockVault(withUID vaultUID: String, rawKey: [UInt8]) throws -> CloudProvider { + let cachedVault = try vaultCache.getCachedVault(withVaultUID: vaultUID) + + guard let vaultConfigToken = cachedVault.vaultConfigToken else { + throw VaultManagerError.missingVaultConfigToken + } + let unverifiedVaultConfig = try UnverifiedVaultConfig(token: vaultConfigToken) + let vaultAccount = try vaultAccountManager.getAccount(with: vaultUID) + let provider = try providerManager.getProvider(with: vaultAccount.delegateAccountUID) + let masterkey = Masterkey.createFromRaw(rawKey: rawKey) + let decorator = try VaultProviderFactory.createVaultProvider(from: unverifiedVaultConfig, + masterkey: masterkey, + vaultPath: vaultAccount.vaultPath, + with: provider) + if masterkeyCacheHelper.shouldCacheMasterkey(forVaultUID: vaultUID) { + try masterkeyCacheManager.cacheMasterkey(masterkey, forVaultUID: vaultUID) + } + return decorator + } + public func createVaultProvider(withUID vaultUID: String, masterkey: Masterkey) throws -> CloudProvider { let cachedVault = try vaultCache.getCachedVault(withVaultUID: vaultUID) let masterkeyFile = try MasterkeyFile.withContentFromData(data: cachedVault.masterkeyFileData) @@ -424,6 +565,16 @@ public class VaultDBManager: VaultManager { } } + /** + Post-processing the vault creation by caching the vault and storing the corresponding master password (if set) in the keychain. + */ + func postProcessVaultCreation(cachedVault: CachedVault, password: String?) throws { + try vaultCache.cache(cachedVault) + if let password = password { + try passwordManager.setPassword(password, forVaultUID: cachedVault.vaultUID) + } + } + func postProcessChangePassphrase(masterkeyFileData: Data, masterkeyFileDataLastModifiedDate: Date?, forVaultUID vaultUID: String, newPassphrase: String) throws { try vaultCache.setMasterkeyFileData(masterkeyFileData, forVaultUID: vaultUID, lastModifiedDate: masterkeyFileDataLastModifiedDate) if try passwordManager.hasPassword(forVaultUID: vaultUID) { @@ -557,3 +708,19 @@ public extension NSFileProviderDomain { self.init(identifier: identifier, displayName: "") } } + +public struct DownloadedVaultConfig { + public let vaultConfig: UnverifiedVaultConfig + let token: Data + let metadata: CloudItemMetadata +} + +public struct DownloadedMasterkeyFile { + let masterkeyFile: MasterkeyFile + let metadata: CloudItemMetadata + let masterkeyFileData: Data +} + +struct PayloadMasterkey: Codable { + let key: String +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultPasswordManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultPasswordManager.swift index 43503810b..d63b54831 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultPasswordManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultPasswordManager.swift @@ -1,5 +1,5 @@ // -// VaultPasswordKeychainManager.swift +// VaultPasswordManager.swift // CryptomatorCommonCore // // Created by Philipp Schmid on 09.07.21. diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FileProviderConnectorMock.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FileProviderConnectorMock.swift index 5457463e3..b1d8152a5 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FileProviderConnectorMock.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FileProviderConnectorMock.swift @@ -64,4 +64,7 @@ final class FileProviderConnectorMock: FileProviderConnector { return Promise(xpc ?? getXPCServiceNameDomainIdentifierReturnValue as! XPC) } } + +// swiftlint:enable all + #endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FullVersionCheckerMock.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FullVersionCheckerMock.swift index d11559613..c6c9dc62d 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FullVersionCheckerMock.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FullVersionCheckerMock.swift @@ -6,11 +6,9 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // -#if DEBUG import Foundation final class FullVersionCheckerMock: FullVersionChecker { var isFullVersion: Bool = false var hasExpiredTrial: Bool = false } -#endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/HubRepositoryMock.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/HubRepositoryMock.swift new file mode 100644 index 000000000..92e0d7896 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/HubRepositoryMock.swift @@ -0,0 +1,53 @@ +import Foundation + +#if DEBUG + +// MARK: - HubRepositoryMock - + +final class HubRepositoryMock: HubRepository { + // MARK: - save + + var saveThrowableError: Error? + var saveCallsCount = 0 + var saveCalled: Bool { + saveCallsCount > 0 + } + + var saveReceivedVault: HubVault? + var saveReceivedInvocations: [HubVault] = [] + var saveClosure: ((HubVault) throws -> Void)? + + func save(_ vault: HubVault) throws { + if let error = saveThrowableError { + throw error + } + saveCallsCount += 1 + saveReceivedVault = vault + saveReceivedInvocations.append(vault) + try saveClosure?(vault) + } + + // MARK: - getHubVault + + var getHubVaultVaultIDThrowableError: Error? + var getHubVaultVaultIDCallsCount = 0 + var getHubVaultVaultIDCalled: Bool { + getHubVaultVaultIDCallsCount > 0 + } + + var getHubVaultVaultIDReceivedVaultID: String? + var getHubVaultVaultIDReceivedInvocations: [String] = [] + var getHubVaultVaultIDReturnValue: HubVault? + var getHubVaultVaultIDClosure: ((String) throws -> HubVault?)? + + func getHubVault(vaultID: String) throws -> HubVault? { + if let error = getHubVaultVaultIDThrowableError { + throw error + } + getHubVaultVaultIDCallsCount += 1 + getHubVaultVaultIDReceivedVaultID = vaultID + getHubVaultVaultIDReceivedInvocations.append(vaultID) + return try getHubVaultVaultIDClosure.map({ try $0(vaultID) }) ?? getHubVaultVaultIDReturnValue + } +} +#endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift index b3ffaaa36..e6b2ef67c 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift @@ -12,7 +12,7 @@ import CryptomatorCryptoLib import Foundation import Promises -// swiftlint:disable all +// swiftlint: disable all final class VaultManagerMock: VaultManager { // MARK: - createNewVault @@ -220,7 +220,53 @@ final class VaultManagerMock: VaultManager { changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedInvocations.append((oldPassphrase: oldPassphrase, newPassphrase: newPassphrase, vaultUID: vaultUID)) return changePassphraseOldPassphraseNewPassphraseForVaultUIDClosure.map({ $0(oldPassphrase, newPassphrase, vaultUID) }) ?? changePassphraseOldPassphraseNewPassphraseForVaultUIDReturnValue } + + // MARK: - addExistingHubVault + + var addExistingHubVaultThrowableError: Error? + var addExistingHubVaultCallsCount = 0 + var addExistingHubVaultCalled: Bool { + addExistingHubVaultCallsCount > 0 + } + + var addExistingHubVaultReceivedVault: ExistingHubVault? + var addExistingHubVaultReceivedInvocations: [ExistingHubVault] = [] + var addExistingHubVaultReturnValue: Promise! + var addExistingHubVaultClosure: ((ExistingHubVault) -> Promise)? + + func addExistingHubVault(_ vault: ExistingHubVault) -> Promise { + if let error = addExistingHubVaultThrowableError { + return Promise(error) + } + addExistingHubVaultCallsCount += 1 + addExistingHubVaultReceivedVault = vault + addExistingHubVaultReceivedInvocations.append(vault) + return addExistingHubVaultClosure.map({ $0(vault) }) ?? addExistingHubVaultReturnValue + } + + // MARK: - manualUnlockVault + + var manualUnlockVaultWithUIDRawKeyThrowableError: Error? + var manualUnlockVaultWithUIDRawKeyCallsCount = 0 + var manualUnlockVaultWithUIDRawKeyCalled: Bool { + manualUnlockVaultWithUIDRawKeyCallsCount > 0 + } + + var manualUnlockVaultWithUIDRawKeyReceivedArguments: (vaultUID: String, rawKey: [UInt8])? + var manualUnlockVaultWithUIDRawKeyReceivedInvocations: [(vaultUID: String, rawKey: [UInt8])] = [] + var manualUnlockVaultWithUIDRawKeyReturnValue: CloudProvider! + var manualUnlockVaultWithUIDRawKeyClosure: ((String, [UInt8]) throws -> CloudProvider)? + + func manualUnlockVault(withUID vaultUID: String, rawKey: [UInt8]) throws -> CloudProvider { + if let error = manualUnlockVaultWithUIDRawKeyThrowableError { + throw error + } + manualUnlockVaultWithUIDRawKeyCallsCount += 1 + manualUnlockVaultWithUIDRawKeyReceivedArguments = (vaultUID: vaultUID, rawKey: rawKey) + manualUnlockVaultWithUIDRawKeyReceivedInvocations.append((vaultUID: vaultUID, rawKey: rawKey)) + return try manualUnlockVaultWithUIDRawKeyClosure.map({ try $0(vaultUID, rawKey) }) ?? manualUnlockVaultWithUIDRawKeyReturnValue + } } -// swiftlint:enable all +// swiftlint: enable all #endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Promise+StructuredConcurrency.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Promise+StructuredConcurrency.swift new file mode 100644 index 000000000..6b8d02d4d --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Promise+StructuredConcurrency.swift @@ -0,0 +1,9 @@ +import Promises + +public extension Promise { + func getValue() async throws -> Value { + try await withCheckedThrowingContinuation({ continuation in + self.then(continuation.resume(returning:)).catch(continuation.resume(throwing:)) + }) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/CryptomatorKeychain+S3.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/CryptomatorKeychain+S3.swift index dc58d48bc..fcabacf2b 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/CryptomatorKeychain+S3.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/CryptomatorKeychain+S3.swift @@ -1,5 +1,5 @@ // -// CryptomatorKeychain+S3.swift.swift +// CryptomatorKeychain+S3.swift // // // Created by Philipp Schmid on 29.06.22. diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/S3CredentialManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/S3CredentialManager.swift index e645ad8dd..5e431b141 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/S3CredentialManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/S3CredentialManager.swift @@ -6,6 +6,7 @@ // import CryptomatorCloudAccessCore +import Dependencies import Foundation import GRDB @@ -41,13 +42,13 @@ public extension CloudProviderAccount { } public struct S3CredentialManager: S3CredentialManagerType { - public static let shared = S3CredentialManager(dbWriter: CryptomatorDatabase.shared.dbPool, keychain: CryptomatorKeychain.s3) - let dbWriter: DatabaseWriter + @Dependency(\.database) var database + public static let shared = S3CredentialManager(keychain: CryptomatorKeychain.s3) let keychain: CryptomatorKeychainType public func save(credential: S3Credential, displayName: String) throws { do { - try dbWriter.write { db in + try database.write { db in let entry = S3DisplayName(id: credential.identifier, displayName: displayName) try entry.save(db) try keychain.saveS3Credential(credential) @@ -56,14 +57,14 @@ public struct S3CredentialManager: S3CredentialManagerType { } public func removeCredential(with identifier: String) throws { - try dbWriter.write { db in + try database.write { db in try S3DisplayName.deleteOne(db, key: ["id": identifier]) try keychain.delete(identifier) } } public func getDisplayName(for identifier: String) throws -> String? { - try dbWriter.read { db in + try database.read { db in let entry = try S3DisplayName.fetchOne(db, key: ["id": identifier]) return entry?.displayName } @@ -84,5 +85,5 @@ extension S3CredentialManager { return inMemoryDB } - public static let demo = S3CredentialManager(dbWriter: inMemoryDB, keychain: CryptomatorKeychain(service: "s3CredentialDemo")) + public static let demo = S3CredentialManager(keychain: CryptomatorKeychain(service: "s3CredentialDemo")) } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+CustomKeyboard.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+CustomKeyboard.swift new file mode 100644 index 000000000..3d4425e91 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+CustomKeyboard.swift @@ -0,0 +1,107 @@ +// +// SwiftUI+CustomKeyboard.swift +// +// +// Created by Philipp Schmid on 23.12.23. +// + +import SwiftUI +import SwiftUIIntrospect + +public extension View { + func backportedSubmitlabel(_ submitLabel: BackportedSubmitLabel) -> some View { + modifier(BackportedSubmitLabelModifier(label: submitLabel)) + } +} + +public enum BackportedSubmitLabel { + /// Defines a submit label with text of "Done". + case done + + /// Defines a submit label with text of "Go". + case go + + /// Defines a submit label with text of "Send". + case send + + /// Defines a submit label with text of "Join". + case join + + /// Defines a submit label with text of "Route". + case route + + /// Defines a submit label with text of "Search". + case search + + /// Defines a submit label with text of "Return". + case `return` + + /// Defines a submit label with text of "Next". + case next + + /// Defines a submit label with text of "Continue". + case `continue` + + @available(iOS 15, *) + var submitLabel: SubmitLabel { + switch self { + case .done: + return .done + case .go: + return .go + case .send: + return .send + case .join: + return .join + case .route: + return .route + case .search: + return .search + case .return: + return .return + case .next: + return .next + case .continue: + return .continue + } + } + + var returnKeyType: UIReturnKeyType { + switch self { + case .done: + return .done + case .go: + return .go + case .send: + return .send + case .join: + return .join + case .route: + return .route + case .search: + return .search + case .return: + return .default + case .next: + return .next + case .continue: + return .continue + } + } +} + +struct BackportedSubmitLabelModifier: ViewModifier { + let label: BackportedSubmitLabel + + public func body(content: Content) -> some View { + if #available(iOS 15, *) { + content + .submitLabel(label.submitLabel) + } else { + content + .introspect(.textField, on: .iOS(.v13, .v14), scope: .ancestor) { textField in + textField.returnKeyType = label.returnKeyType + } + } + } +} diff --git a/Cryptomator/Common/SwiftUI+Focus.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+Focus.swift similarity index 100% rename from Cryptomator/Common/SwiftUI+Focus.swift rename to CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+Focus.swift diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+ListBackground.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+ListBackground.swift new file mode 100644 index 000000000..779e849e3 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+ListBackground.swift @@ -0,0 +1,25 @@ +import Introspect +import SwiftUI + +public extension View { + func setListBackgroundColor(_ color: Color) -> some View { + modifier(ListBackgroundModifier(color: color)) + } +} + +struct ListBackgroundModifier: ViewModifier { + let color: Color + + public func body(content: Content) -> some View { + if #available(iOS 16, *) { + content + .scrollContentBackground(.hidden) + .background(color) + } else { + content + .introspectTableView { + $0.backgroundColor = UIColor(color) + } + } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/UIColor+CryptomatorColors.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/UIColor+CryptomatorColors.swift index 86deea7f6..aec402502 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/UIColor+CryptomatorColors.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/UIColor+CryptomatorColors.swift @@ -6,6 +6,7 @@ // Copyright © 2022 Skymatic GmbH. All rights reserved. // +import SwiftUI import UIKit public extension UIColor { @@ -21,3 +22,9 @@ public extension UIColor { return UIColor(named: "yellow")! } } + +public extension Color { + static var cryptomatorPrimary: Color { Color(UIColor.cryptomatorPrimary) } + static var cryptomatorBackground: Color { Color(UIColor.cryptomatorBackground) } + static var cryptomatorYellow: Color { Color(UIColor.cryptomatorYellow) } +} diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/FullVersionCheckerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/FullVersionCheckerTests.swift index 574f3eabd..eec8fc2ca 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/FullVersionCheckerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/FullVersionCheckerTests.swift @@ -7,6 +7,7 @@ import XCTest @testable import CryptomatorCommonCore +@testable import Dependencies class FullVersionCheckerTests: XCTestCase { var settingsMock: CryptomatorSettingsMock! @@ -14,10 +15,11 @@ class FullVersionCheckerTests: XCTestCase { override func setUpWithError() throws { settingsMock = CryptomatorSettingsMock() + DependencyValues.mockDependency(\.cryptomatorSettings, with: settingsMock) settingsMock.fullVersionUnlocked = false settingsMock.hasRunningSubscription = false settingsMock.trialExpirationDate = nil - fullVersionChecker = UserDefaultsFullVersionChecker(cryptomatorSettings: settingsMock) + fullVersionChecker = UserDefaultsFullVersionChecker() } // MARK: Is Full Version diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/AddHubVaultUnlockHandlerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/AddHubVaultUnlockHandlerTests.swift new file mode 100644 index 000000000..1f2601b0e --- /dev/null +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/AddHubVaultUnlockHandlerTests.swift @@ -0,0 +1,110 @@ +// +// AddHubVaultUnlockHandlerTests.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 19.11.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. + +import JOSESwift +import Promises +import XCTest +@testable import CryptomatorCloudAccessCore +@testable import CryptomatorCommonCore +@testable import CryptomatorCryptoLib + +final class AddHubVaultUnlockHandlerTests: XCTestCase { + private let vaultUID = "vault-123456789" + private let accountUID = "account-123456789" + private var vaultManagerMock: VaultManagerMock! + private var unlockHandlerDelegateMock: HubVaultUnlockHandlerDelegateMock! + + override func setUpWithError() throws { + vaultManagerMock = VaultManagerMock() + unlockHandlerDelegateMock = HubVaultUnlockHandlerDelegateMock() + } + + func testDidSuccessfullyRemoteUnlock() async throws { + let vaultConfig = VaultConfig(id: "ABB9F673-F3E8-41A7-A43B-D29F5DA65068", format: 8, cipherCombo: .sivCtrMac, shorteningThreshold: 220) + let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) + + let token = try vaultConfig.toToken(keyId: "masterkeyfile:masterkey.cryptomator", rawKey: masterkey.rawKey) + let unverifiedVaultConfig = try UnverifiedVaultConfig(token: token) + let metadata = CloudItemMetadata(name: "masterkey.cryptomator", + cloudPath: .init("/masterkey.cryptomator"), + itemType: .file, + lastModifiedDate: nil, + size: nil) + let jwe = try JWE(compactSerialization: "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJjcnYiOiJQLTM4NCIsImV4dCI6dHJ1ZSwia2V5X29wcyI6W10sImt0eSI6IkVDIiwieCI6Im9DLWlIcDhjZzVsUy1Qd3JjRjZxS0NzbWxfMFJzaEtCV0JJTUYzVjhuTGg2NGlCWTdsX0VsZ3Fjd0JZLXNsR3IiLCJ5IjoiVWozVzdYYVBQakJiMFRwWUFHeXlweVRIR3ByQU1hRXdWTk5Gb05tNEJuNjZuVkNKLU9pUUJYN3RhaVUtby1yWSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ.._r7LC8HLc00jk2SI.ooeI0-E29jryMJ_wbGWKVc_IfHOh3Mlfh5geRYEmLTA4GKHItRYmDdZvGsCj9pJRoNORyHdmlAMxXXIXq_v9ZocoCwZrN7EsaB8A3Kukka35i1sr7kpNbksk3G_COsGRmwQ.GJCKBE-OZ7Nm5RMf_9UwVg") + + let privateKeyPemRepresentation = "-----BEGIN PRIVATE KEY-----\nMIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDqcfr7I2SUcaYK/QHn\njhDGMpoAI1VBqzlGQ+QENqkwmGsk7N/mIQ3IJp5o7avKNJehZANiAASbOrmxoDPp\nb4AuVnUCyE1nw9KzDluGH8rozjUrteMS8ntzNlzK218iJgpRi6I3rLs8IoWTHrGE\nkfgDMgV4fk+7OC8AlKdofJudF/YcBsC00bhQ2lhlEP+PtcpgkkcJbAI=\n-----END PRIVATE KEY-----" + + let downloadedVaultConfig = DownloadedVaultConfig(vaultConfig: unverifiedVaultConfig, + token: token, + metadata: metadata) + let unlockHandler = AddHubVaultUnlockHandler(vaultUID: vaultUID, + accountUID: accountUID, vaultItem: VaultItemStub(), downloadedVaultConfig: downloadedVaultConfig, + vaultManager: vaultManagerMock, + delegate: unlockHandlerDelegateMock) + vaultManagerMock.addExistingHubVaultReturnValue = Promise(()) + + // WHEN + // calling didSuccessfullyRemoteUnlock + try await unlockHandler.didSuccessfullyRemoteUnlock(.init(jwe: jwe, privateKey: .init(pemRepresentation: privateKeyPemRepresentation), subscriptionState: .active)) + + // THEN + // the hub vault has been added as an existing one + let savedHubVault = vaultManagerMock.addExistingHubVaultReceivedVault + XCTAssertEqual(savedHubVault?.vaultUID, vaultUID) + XCTAssertEqual(savedHubVault?.delegateAccountUID, accountUID) + XCTAssertEqual(savedHubVault?.jweData, jwe.compactSerializedData) + XCTAssertEqual(savedHubVault?.downloadedVaultConfig.token, token) + + // and the delegate gets informed that the handler successfully processed the unlocked vault + XCTAssertEqual(unlockHandlerDelegateMock.successfullyProcessedUnlockedVaultCallsCount, 1) + XCTAssertFalse(unlockHandlerDelegateMock.failedToProcessUnlockedVaultErrorCalled) + } + + func testDidSuccessfullyRemoteUnlock_fails_informsDelegateAboutFailure() async throws { + let vaultConfig = VaultConfig(id: "ABB9F673-F3E8-41A7-A43B-D29F5DA65068", format: 8, cipherCombo: .sivCtrMac, shorteningThreshold: 220) + let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) + + let token = try vaultConfig.toToken(keyId: "masterkeyfile:masterkey.cryptomator", rawKey: masterkey.rawKey) + let unverifiedVaultConfig = try UnverifiedVaultConfig(token: token) + let metadata = CloudItemMetadata(name: "masterkey.cryptomator", + cloudPath: .init("/masterkey.cryptomator"), + itemType: .file, + lastModifiedDate: nil, + size: nil) + let jwe = try JWE(compactSerialization: "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJjcnYiOiJQLTM4NCIsImV4dCI6dHJ1ZSwia2V5X29wcyI6W10sImt0eSI6IkVDIiwieCI6Im9DLWlIcDhjZzVsUy1Qd3JjRjZxS0NzbWxfMFJzaEtCV0JJTUYzVjhuTGg2NGlCWTdsX0VsZ3Fjd0JZLXNsR3IiLCJ5IjoiVWozVzdYYVBQakJiMFRwWUFHeXlweVRIR3ByQU1hRXdWTk5Gb05tNEJuNjZuVkNKLU9pUUJYN3RhaVUtby1yWSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ.._r7LC8HLc00jk2SI.ooeI0-E29jryMJ_wbGWKVc_IfHOh3Mlfh5geRYEmLTA4GKHItRYmDdZvGsCj9pJRoNORyHdmlAMxXXIXq_v9ZocoCwZrN7EsaB8A3Kukka35i1sr7kpNbksk3G_COsGRmwQ.GJCKBE-OZ7Nm5RMf_9UwVg") + + let privateKeyPemRepresentation = "-----BEGIN PRIVATE KEY-----\nMIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDqcfr7I2SUcaYK/QHn\njhDGMpoAI1VBqzlGQ+QENqkwmGsk7N/mIQ3IJp5o7avKNJehZANiAASbOrmxoDPp\nb4AuVnUCyE1nw9KzDluGH8rozjUrteMS8ntzNlzK218iJgpRi6I3rLs8IoWTHrGE\nkfgDMgV4fk+7OC8AlKdofJudF/YcBsC00bhQ2lhlEP+PtcpgkkcJbAI=\n-----END PRIVATE KEY-----" + + let downloadedVaultConfig = DownloadedVaultConfig(vaultConfig: unverifiedVaultConfig, + token: token, + metadata: metadata) + let unlockHandler = AddHubVaultUnlockHandler(vaultUID: vaultUID, + accountUID: accountUID, vaultItem: VaultItemStub(), downloadedVaultConfig: downloadedVaultConfig, + vaultManager: vaultManagerMock, + delegate: unlockHandlerDelegateMock) + // GIVEN + // the existing hub vault can't be added due to an error + vaultManagerMock.addExistingHubVaultReturnValue = Promise(TestError()) + + // WHEN + // calling didSuccessfullyRemoteUnlock + try await unlockHandler.didSuccessfullyRemoteUnlock(.init(jwe: jwe, privateKey: .init(pemRepresentation: privateKeyPemRepresentation), subscriptionState: .active)) + + // THEN + // the delegate gets informed that the handler failed to process the unlocked vault + XCTAssertEqual(unlockHandlerDelegateMock.failedToProcessUnlockedVaultErrorCallsCount, 1) + XCTAssert(unlockHandlerDelegateMock.failedToProcessUnlockedVaultErrorReceivedError is TestError) + XCTAssertFalse(unlockHandlerDelegateMock.successfullyProcessedUnlockedVaultCalled) + } + + private struct VaultItemStub: VaultItem { + let name = "name" + let vaultPath = CloudPath("/name") + } + + private struct TestError: Error {} +} diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift new file mode 100644 index 000000000..a0710a866 --- /dev/null +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift @@ -0,0 +1,352 @@ +// +// HubAuthenticationViewModelTests.swift +// +// +// Created by Philipp Schmid on 19.11.23. +// + +import AppAuthCore +import CryptoKit +import JOSESwift +import XCTest +@testable import CryptomatorCloudAccessCore +@testable import CryptomatorCommonCore +@testable import CryptomatorCryptoLib +@testable import Dependencies + +final class HubAuthenticationViewModelTests: XCTestCase { + private var unlockHandlerMock: HubVaultUnlockHandlerMock! + private var delegateMock: HubAuthenticationViewModelDelegateMock! + private var hubKeyServiceMock: HubKeyReceivingMock! + private var viewModel: HubAuthenticationViewModel! + + override func setUpWithError() throws { + unlockHandlerMock = HubVaultUnlockHandlerMock() + delegateMock = HubAuthenticationViewModelDelegateMock() + hubKeyServiceMock = HubKeyReceivingMock() + + let unverifiedVaultConfig = try UnverifiedVaultConfig(token: validHubVaultConfig()) + + viewModel = HubAuthenticationViewModel(authState: .stub, + vaultConfig: unverifiedVaultConfig, + unlockHandler: unlockHandlerMock, + delegate: delegateMock) + } + + // MARK: continueToAccessCheck + + func testContinueToAccessCheck_showsLoadingSpinnerWhileReceivingKey() async throws { + XCTAssertFalse(delegateMock.hubAuthenticationViewModelWantsToShowLoadingIndicatorCalled) + XCTAssertFalse(delegateMock.hubAuthenticationViewModelWantsToHideLoadingIndicatorCalled) + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + let hubKeyProviderMock = CryptomatorHubKeyProviderMock() + DependencyValues.mockDependency(\.cryptomatorHubKeyProvider, with: hubKeyProviderMock) + hubKeyProviderMock.getPrivateKeyReturnValue = P384.KeyAgreement.PrivateKey(compactRepresentable: false) + + let calledReceiveKey = XCTestExpectation() + hubKeyServiceMock.receiveKeyAuthStateVaultConfigClosure = { _, _ in + calledReceiveKey.fulfill() + return try .successMock() + } + + let calledShowLoadingIndicator = XCTestExpectation() + delegateMock.hubAuthenticationViewModelWantsToShowLoadingIndicatorClosure = { + calledShowLoadingIndicator.fulfill() + } + + let calledHideLoadingIndicator = XCTestExpectation() + delegateMock.hubAuthenticationViewModelWantsToHideLoadingIndicatorClosure = { + calledHideLoadingIndicator.fulfill() + } + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the loading indicator should be displayed while receiving the key + await fulfillment(of: [calledShowLoadingIndicator, calledReceiveKey, calledHideLoadingIndicator], enforceOrder: true) + } + + func testContinueToAccessCheck_showsLoadingSpinnerWhileReceivingKeyHidesIfFailed() async throws { + XCTAssertFalse(delegateMock.hubAuthenticationViewModelWantsToShowLoadingIndicatorCalled) + XCTAssertFalse(delegateMock.hubAuthenticationViewModelWantsToHideLoadingIndicatorCalled) + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + let calledReceiveKey = XCTestExpectation() + hubKeyServiceMock.receiveKeyAuthStateVaultConfigClosure = { _, _ in + calledReceiveKey.fulfill() + throw TestError() + } + + let calledShowLoadingIndicator = XCTestExpectation() + delegateMock.hubAuthenticationViewModelWantsToShowLoadingIndicatorClosure = { + calledShowLoadingIndicator.fulfill() + } + + let calledHideLoadingIndicator = XCTestExpectation() + delegateMock.hubAuthenticationViewModelWantsToHideLoadingIndicatorClosure = { + calledHideLoadingIndicator.fulfill() + } + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the loading indicator should be displayed while receiving the key and gets hidden even if the operation fails + await fulfillment(of: [calledShowLoadingIndicator, calledReceiveKey, calledHideLoadingIndicator], enforceOrder: true) + } + + func testContinueToAccessCheck_success_hubSubscriptionStateIsActive() async throws { + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + let hubKeyProviderMock = CryptomatorHubKeyProviderMock() + DependencyValues.mockDependency(\.cryptomatorHubKeyProvider, with: hubKeyProviderMock) + + // GIVEN + // the hub key service returns success with an active Cryptomator Hub subscription state + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = try .successMock(header: ["hub-subscription-state": "ACTIVE"]) + + let devicePrivKey = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB2bmFCWy2p+EbAn8NWS5Om+GA7c5LHhRZb8g2pSMSf0fsd7k7dZDVrnyHFiLdd/YGhZANiAAR6bsjTEdXKWIuu1Bvj6Y8wySlIROy7YpmVZTY128ItovCD8pcR4PnFljvAIb2MshCdr1alX4g6cgDOqcTeREiObcSfucOU9Ry1pJ/GnX6KA0eSljrk6rxjSDos8aiZ6Mg=" + let data = Data(base64Encoded: devicePrivKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + hubKeyProviderMock.getPrivateKeyReturnValue = privateKey + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the unlock handler gets informed about the successful remote unlock with an active Cryptomator Hub subscription state + let receivedResponse = unlockHandlerMock.didSuccessfullyRemoteUnlockReceivedResponse + XCTAssertEqual(unlockHandlerMock.didSuccessfullyRemoteUnlockCallsCount, 1) + XCTAssertEqual(receivedResponse?.subscriptionState, .active) + } + + func testContinueToAccessCheck_success_hubSubscriptionStateIsInactive() async throws { + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + let hubKeyProviderMock = CryptomatorHubKeyProviderMock() + DependencyValues.mockDependency(\.cryptomatorHubKeyProvider, with: hubKeyProviderMock) + + // GIVEN + // the hub key service returns success with an active Cryptomator Hub subscription state + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = try .successMock(header: ["hub-subscription-state": "INACTIVE"]) + + let devicePrivKey = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB2bmFCWy2p+EbAn8NWS5Om+GA7c5LHhRZb8g2pSMSf0fsd7k7dZDVrnyHFiLdd/YGhZANiAAR6bsjTEdXKWIuu1Bvj6Y8wySlIROy7YpmVZTY128ItovCD8pcR4PnFljvAIb2MshCdr1alX4g6cgDOqcTeREiObcSfucOU9Ry1pJ/GnX6KA0eSljrk6rxjSDos8aiZ6Mg=" + let data = Data(base64Encoded: devicePrivKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + hubKeyProviderMock.getPrivateKeyReturnValue = privateKey + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the unlock handler gets informed about the successful remote unlock with an inactive Cryptomator Hub subscription state + let receivedResponse = unlockHandlerMock.didSuccessfullyRemoteUnlockReceivedResponse + XCTAssertEqual(unlockHandlerMock.didSuccessfullyRemoteUnlockCallsCount, 1) + XCTAssertEqual(receivedResponse?.subscriptionState, .inactive) + } + + func testContinueToAccessCheck_success_hubSubscriptionStateIsUnknown() async throws { + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + let hubKeyProviderMock = CryptomatorHubKeyProviderMock() + DependencyValues.mockDependency(\.cryptomatorHubKeyProvider, with: hubKeyProviderMock) + + // GIVEN + // the hub key service returns success with an unknown Cryptomator Hub subscription state + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = try .successMock(header: ["hub-subscription-state": "foo"]) + hubKeyProviderMock.getPrivateKeyReturnValue = P384.KeyAgreement.PrivateKey(compactRepresentable: false) + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the unlock handler gets not informed about a successful remote unlock + XCTAssertFalse(unlockHandlerMock.didSuccessfullyRemoteUnlockCalled) + // the user gets informed about the error + let currentAuthenticationFlowState = try XCTUnwrap(viewModel.authenticationFlowState) + XCTAssert(currentAuthenticationFlowState.isError) + } + + func testContinueToAccessCheck_accessNotGranted() async throws { + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + + // GIVEN + // the hub key service returns access not granted + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = .accessNotGranted + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the authentication flow state is set to accessNotGranted + XCTAssertEqual(viewModel.authenticationFlowState, .accessNotGranted) + } + + func testContinueToAccessCheck_needsDeviceRegistration() async throws { + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + + // GIVEN + // the hub key service returns needs device registration + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = .needsDeviceRegistration + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the authentication flow state is set to needsDeviceRegistration where the user needs to set the device name + XCTAssertEqual(viewModel.authenticationFlowState, .deviceRegistration(.deviceName)) + } + + func testContinueToAccessCheck_licenseExceeded() async throws { + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + + // GIVEN + // the hub key service returns that the Cryptomator Hub License is exceeded + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = .licenseExceeded + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the authentication flow state is set to licenseExceeded + XCTAssertEqual(viewModel.authenticationFlowState, .licenseExceeded) + } + + // MARK: Register + + func testRegister_registersDevice_withName() async { + let deviceRegisteringMock = HubDeviceRegisteringMock() + DependencyValues.mockDependency(\.hubDeviceRegisteringService, with: deviceRegisteringMock) + + // GIVEN + // a name has been set by the user + viewModel.deviceName = "My Device 123" + + // WHEN + // the user taps on register + await viewModel.register() + + // THEN + // the registerDevice got called on the device registering servie + let receivedArguments = deviceRegisteringMock.registerDeviceWithNameHubConfigAuthStateSetupCodeReceivedArguments + XCTAssertEqual(deviceRegisteringMock.registerDeviceWithNameHubConfigAuthStateSetupCodeCallsCount, 1) + // with the name set by the user + XCTAssertEqual(receivedArguments?.name, "My Device 123") + } + + private struct TestError: Error {} + + private func validHubVaultConfig() -> Data { + "eyJraWQiOiJodWIraHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcvaHViMzAvYXBpL3ZhdWx0cy83NWFmMjFiNy00ODQ5LTQ1NTgtYjA1Yy1kZTZkYzkwNzdhNjciLCJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiIsImh1YiI6eyJjbGllbnRJZCI6ImNyeXB0b21hdG9yIiwiYXV0aEVuZHBvaW50IjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcva2MvcmVhbG1zL2h1YjMwL3Byb3RvY29sL29wZW5pZC1jb25uZWN0L2F1dGgiLCJ0b2tlbkVuZHBvaW50IjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcva2MvcmVhbG1zL2h1YjMwL3Byb3RvY29sL29wZW5pZC1jb25uZWN0L3Rva2VuIiwiYXV0aFN1Y2Nlc3NVcmwiOiJodHRwczovL3Rlc3RpbmcuaHViLmNyeXB0b21hdG9yLm9yZy9odWIzMC9hcHAvdW5sb2NrLXN1Y2Nlc3M_dmF1bHQ9NzVhZjIxYjctNDg0OS00NTU4LWIwNWMtZGU2ZGM5MDc3YTY3IiwiYXV0aEVycm9yVXJsIjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcvaHViMzAvYXBwL3VubG9jay1lcnJvcj92YXVsdD03NWFmMjFiNy00ODQ5LTQ1NTgtYjA1Yy1kZTZkYzkwNzdhNjciLCJhcGlCYXNlVXJsIjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcvaHViMzAvYXBpLyIsImRldmljZXNSZXNvdXJjZVVybCI6Imh0dHBzOi8vdGVzdGluZy5odWIuY3J5cHRvbWF0b3Iub3JnL2h1YjMwL2FwaS9kZXZpY2VzLyJ9fQ.eyJqdGkiOiI3NWFmMjFiNy00ODQ5LTQ1NTgtYjA1Yy1kZTZkYzkwNzdhNjciLCJmb3JtYXQiOjgsImNpcGhlckNvbWJvIjoiU0lWX0dDTSIsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMH0.Z0x_5D073zo3smZq5q5wgDRheewcapCrIqg_0iD5qwM".data(using: .utf8)! + } + + private func validHubResponseData() -> Data { + "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJjcnYiOiJQLTM4NCIsImV4dCI6dHJ1ZSwia2V5X29wcyI6W10sImt0eSI6IkVDIiwieCI6Im9DLWlIcDhjZzVsUy1Qd3JjRjZxS0NzbWxfMFJzaEtCV0JJTUYzVjhuTGg2NGlCWTdsX0VsZ3Fjd0JZLXNsR3IiLCJ5IjoiVWozVzdYYVBQakJiMFRwWUFHeXlweVRIR3ByQU1hRXdWTk5Gb05tNEJuNjZuVkNKLU9pUUJYN3RhaVUtby1yWSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ.._r7LC8HLc00jk2SI.ooeI0-E29jryMJ_wbGWKVc_IfHOh3Mlfh5geRYEmLTA4GKHItRYmDdZvGsCj9pJRoNORyHdmlAMxXXIXq_v9ZocoCwZrN7EsaB8A3Kukka35i1sr7kpNbksk3G_COsGRmwQ.GJCKBE-OZ7Nm5RMf_9UwVg".data(using: .utf8)! + } +} + +private extension OIDAuthState { + static var stub: Self { + .init(authorizationResponse: .init(request: .init(configuration: .init(authorizationEndpoint: URL(string: "example.com")!, tokenEndpoint: URL(string: "example.com")!), clientId: "", scopes: nil, redirectURL: URL(string: "example.com")!, responseType: "code", additionalParameters: nil), parameters: [:])) + } +} + +private extension HubAuthenticationViewModel.State { + var isError: Bool { + switch self { + case .error: + return true + default: + return false + } + } +} + +private extension JWE { + static func encryptedUserKeyStub() throws -> JWE { + try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrZXlfb3BzIjpbXSwiZXh0Ijp\ + 0cnVlLCJrdHkiOiJFQyIsIngiOiJoeHpiSWh6SUJza3A5ZkZFUmJSQ2RfOU1fbWYxNElqaDZhcnNoVX\ + NkcEEyWno5ejZZNUs4NHpZR2I4b2FHemNUIiwieSI6ImJrMGRaNWhpelZ0TF9hN2hNejBjTUduNjhIR\ + jZFdWlyNHdlclNkTFV5QWd2NWUzVzNYSG5sdHJ2VlRyU3pzUWYiLCJjcnYiOiJQLTM4NCJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..pu3Q1nR_yvgRAapG.4zW0xm0JPxbcvZ66R-Mn3k841lHelDQfaUvsZZAtWs\ + L2w4FMi6H_uu6ArAWYLtNREa_zfcPuyuJsFferYPSNRUWt4OW6aWs-l_wfo7G1ceEVxztQXzQiwD30U\ + TA8OOdPcUuFfEq2-d9217jezrcyO6m6FjyssEZIrnRArUPWKzGdghXccGkkf0LTZcGJoHeKal-RtyP8\ + PfvEAWTjSOCpBlSdUJ-1JL3tyd97uVFNaVuH3i7vvcMoUP_bdr0XW3rvRgaeC6X4daPLUvR1hK5Msut\ + QMtM2vpFghS_zZxIQRqz3B2ECxa9Bjxhmn8kLX5heZ8fq3lH-bmJp1DxzZ4V1RkWk.yVwXG9yARa5Ih\ + q2koh2NbQ + """) + } + + static func encryptedVaultKeyStub() throws -> JWE { + try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImNZdlVFZm9LYkJjenZySE5zQjUxOGpycUxPMGJDOW5lZjR4NzFFMUQ5dk95MXRqd1piZzV3cFI0OE5nU1RQdHgiLCJ5IjoiaWRJekhCWERzSzR2NTZEeU9yczJOcDZsSG1zb29fMXV0VTlzX3JNdVVkbkxuVXIzUXdLZkhYMWdaVXREM1RKayJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..0VZqu5ei9U3blGtq.eDvhU6drw7mIwvXu6Q.f05QnhI7JWG3IYHvexwdFQ + """) + } +} + +private extension HubAuthenticationFlow { + static func successMock(header: [AnyHashable: Any] = [:]) throws -> HubAuthenticationFlow { + try .success(.init(encryptedUserKey: .encryptedUserKeyStub(), + encryptedVaultKey: .encryptedVaultKeyStub(), + header: header)) + } +} + +// MARK: - HubAuthenticationViewModelDelegateMock - + +// swiftlint: disable all +final class HubAuthenticationViewModelDelegateMock: HubAuthenticationViewModelDelegate { + // MARK: - hubAuthenticationViewModelWantsToShowLoadingIndicator + + var hubAuthenticationViewModelWantsToShowLoadingIndicatorCallsCount = 0 + var hubAuthenticationViewModelWantsToShowLoadingIndicatorCalled: Bool { + hubAuthenticationViewModelWantsToShowLoadingIndicatorCallsCount > 0 + } + + var hubAuthenticationViewModelWantsToShowLoadingIndicatorClosure: (() -> Void)? + + func hubAuthenticationViewModelWantsToShowLoadingIndicator() { + hubAuthenticationViewModelWantsToShowLoadingIndicatorCallsCount += 1 + hubAuthenticationViewModelWantsToShowLoadingIndicatorClosure?() + } + + // MARK: - hubAuthenticationViewModelWantsToHideLoadingIndicator + + var hubAuthenticationViewModelWantsToHideLoadingIndicatorCallsCount = 0 + var hubAuthenticationViewModelWantsToHideLoadingIndicatorCalled: Bool { + hubAuthenticationViewModelWantsToHideLoadingIndicatorCallsCount > 0 + } + + var hubAuthenticationViewModelWantsToHideLoadingIndicatorClosure: (() -> Void)? + + func hubAuthenticationViewModelWantsToHideLoadingIndicator() { + hubAuthenticationViewModelWantsToHideLoadingIndicatorCallsCount += 1 + hubAuthenticationViewModelWantsToHideLoadingIndicatorClosure?() + } + + // MARK: - hubAuthenticationViewModelWantsToShowNeedsAccountInitAlert + + var hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLCallsCount = 0 + var hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLCalled: Bool { + hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLCallsCount > 0 + } + + var hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLReceivedProfileURL: URL? + var hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLReceivedInvocations: [URL] = [] + var hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLClosure: ((URL) -> Void)? + + func hubAuthenticationViewModelWantsToShowNeedsAccountInitAlert(profileURL: URL) { + hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLCallsCount += 1 + hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLReceivedProfileURL = profileURL + hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLReceivedInvocations.append(profileURL) + hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLClosure?(profileURL) + } +} + +// swiftlint: enable all diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubDBRepositoryTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubDBRepositoryTests.swift new file mode 100644 index 000000000..211b2f87f --- /dev/null +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubDBRepositoryTests.swift @@ -0,0 +1,89 @@ +import GRDB +import XCTest +@testable import CryptomatorCommonCore + +final class HubDBRepositoryTests: XCTestCase { + private var inMemoryDB: DatabaseQueue! + private var repository: HubDBRepository! + private var vaultAccountManager: VaultAccountManager! + private var cloudAccountManager: CloudProviderAccountManager! + + override func setUpWithError() throws { + repository = HubDBRepository() + vaultAccountManager = VaultAccountDBManager() + cloudAccountManager = CloudProviderAccountDBManager() + } + + func testSaveAndRetrieve() throws { + // GIVEN + // a cloud account has been created + let cloudAccount = CloudProviderAccount(accountUID: "", cloudProviderType: .dropbox) + try cloudAccountManager.saveNewAccount(cloudAccount) + + // and a vault account has been created + let vaultID = "123456789" + let vaultAccount = VaultAccount(vaultUID: vaultID, delegateAccountUID: "", vaultPath: .init(""), vaultName: "") + try vaultAccountManager.saveNewAccount(vaultAccount) + + // WHEN + // saving a hub vault + let vault = HubVault(vaultUID: vaultID, subscriptionState: .active) + try repository.save(vault) + + // THEN + // it can be retrieved + let retrievedVault = try repository.getHubVault(vaultID: vaultID) + XCTAssertEqual(vault, retrievedVault) + } + + func testSaveToUpdate() throws { + // GIVEN + // a cloud account has been created + let cloudAccount = CloudProviderAccount(accountUID: "", cloudProviderType: .dropbox) + try cloudAccountManager.saveNewAccount(cloudAccount) + + // and a vault account has been created + let vaultID = "123456789" + let vaultAccount = VaultAccount(vaultUID: vaultID, delegateAccountUID: "", vaultPath: .init(""), vaultName: "") + try vaultAccountManager.saveNewAccount(vaultAccount) + + // WHEN + // saving a hub vault + let initialVault = HubVault(vaultUID: vaultID, subscriptionState: .active) + try repository.save(initialVault) + + // and saving the hub vault with the same vault ID but a changed subscription state + let updatedVault = HubVault(vaultUID: vaultID, subscriptionState: .inactive) + try repository.save(updatedVault) + + // THEN + // it the updated version can be retrieved + let retrievedVault = try repository.getHubVault(vaultID: vaultID) + XCTAssertEqual(updatedVault, retrievedVault) + } + + func testDeleteVaultAccountAlsoDeletesHubVault() throws { + // GIVEN + // a cloud account has been created + let cloudAccount = CloudProviderAccount(accountUID: "", cloudProviderType: .dropbox) + try cloudAccountManager.saveNewAccount(cloudAccount) + + // and a vault account has been created + let vaultID = "123456789" + let vaultAccount = VaultAccount(vaultUID: vaultID, delegateAccountUID: "", vaultPath: .init(""), vaultName: "") + try vaultAccountManager.saveNewAccount(vaultAccount) + + // and a hub vault has been created for the vault id + let vault = HubVault(vaultUID: vaultID, subscriptionState: .active) + try repository.save(vault) + + // WHEN + // the vault account gets deleted + try vaultAccountManager.removeAccount(with: vaultID) + + // THEN + // the hub vault account has been deleted and can not be retrieved + let retrievedVault = try repository.getHubVault(vaultID: vaultID) + XCTAssertNil(retrievedVault) + } +} diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubVaultUnlockHandlerDelegateMock.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubVaultUnlockHandlerDelegateMock.swift new file mode 100644 index 000000000..aa6ec4fb1 --- /dev/null +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubVaultUnlockHandlerDelegateMock.swift @@ -0,0 +1,45 @@ +// +// HubVaultUnlockHandlerDelegateMock.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 19.11.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. + +import Foundation +@testable import CryptomatorCommonCore +// swiftlint:disable all +final class HubVaultUnlockHandlerDelegateMock: HubVaultUnlockHandlerDelegate { + // MARK: - successfullyProcessedUnlockedVault + + var successfullyProcessedUnlockedVaultCallsCount = 0 + var successfullyProcessedUnlockedVaultCalled: Bool { + successfullyProcessedUnlockedVaultCallsCount > 0 + } + + var successfullyProcessedUnlockedVaultClosure: (() -> Void)? + + func successfullyProcessedUnlockedVault() { + successfullyProcessedUnlockedVaultCallsCount += 1 + successfullyProcessedUnlockedVaultClosure?() + } + + // MARK: - failedToProcessUnlockedVault + + var failedToProcessUnlockedVaultErrorCallsCount = 0 + var failedToProcessUnlockedVaultErrorCalled: Bool { + failedToProcessUnlockedVaultErrorCallsCount > 0 + } + + var failedToProcessUnlockedVaultErrorReceivedError: Error? + var failedToProcessUnlockedVaultErrorReceivedInvocations: [Error] = [] + var failedToProcessUnlockedVaultErrorClosure: ((Error) -> Void)? + + func failedToProcessUnlockedVault(error: Error) { + failedToProcessUnlockedVaultErrorCallsCount += 1 + failedToProcessUnlockedVaultErrorReceivedError = error + failedToProcessUnlockedVaultErrorReceivedInvocations.append(error) + failedToProcessUnlockedVaultErrorClosure?(error) + } +} + +// swiftlint:enable all diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift new file mode 100644 index 000000000..311bb051c --- /dev/null +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift @@ -0,0 +1,221 @@ +// +// JWEHelperTests.swift +// CryptomatorCommonCoreTests +// +// Created by Philipp Schmid on 25.12.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. + +import CryptoKit +import CryptomatorCommonCore +import JOSESwift +import SwiftECC +import XCTest +@testable import CryptomatorCryptoLib + +final class JWEHelperTests: XCTestCase { + // key pairs from frontend tests (crypto.spec.ts): + private let userPrivKey = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDCi4K1Ts3DgTz/ufkLX7EGMHjGpJv+WJmFgyzLwwaDFSfLpDw0Kgf3FKK+LAsV8r+hZANiAARLOtFebIjxVYUmDV09Q1sVxz2Nm+NkR8fu6UojVSRcCW13tEZatx8XGrIY9zC7oBCEdRqDc68PMSvS5RA0Pg9cdBNc/kgMZ1iEmEv5YsqOcaNADDSs0bLlXb35pX7Kx5Y=" + private let devicePrivKey = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB2bmFCWy2p+EbAn8NWS5Om+GA7c5LHhRZb8g2pSMSf0fsd7k7dZDVrnyHFiLdd/YGhZANiAAR6bsjTEdXKWIuu1Bvj6Y8wySlIROy7YpmVZTY128ItovCD8pcR4PnFljvAIb2MshCdr1alX4g6cgDOqcTeREiObcSfucOU9Ry1pJ/GnX6KA0eSljrk6rxjSDos8aiZ6Mg=" + + // used for JWE generation in frontend: (jwe.spec.ts): + private let privKey = "ME8CAQAwEAYHKoZIzj0CAQYFK4EEACIEODA2AgEBBDEA6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ" + + override func setUpWithError() throws {} + + func testDecryptUserKeyECDHES() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrZXlfb3BzIjpbXSwiZXh0Ijp\ + 0cnVlLCJrdHkiOiJFQyIsIngiOiJoeHpiSWh6SUJza3A5ZkZFUmJSQ2RfOU1fbWYxNElqaDZhcnNoVX\ + NkcEEyWno5ejZZNUs4NHpZR2I4b2FHemNUIiwieSI6ImJrMGRaNWhpelZ0TF9hN2hNejBjTUduNjhIR\ + jZFdWlyNHdlclNkTFV5QWd2NWUzVzNYSG5sdHJ2VlRyU3pzUWYiLCJjcnYiOiJQLTM4NCJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..pu3Q1nR_yvgRAapG.4zW0xm0JPxbcvZ66R-Mn3k841lHelDQfaUvsZZAtWs\ + L2w4FMi6H_uu6ArAWYLtNREa_zfcPuyuJsFferYPSNRUWt4OW6aWs-l_wfo7G1ceEVxztQXzQiwD30U\ + TA8OOdPcUuFfEq2-d9217jezrcyO6m6FjyssEZIrnRArUPWKzGdghXccGkkf0LTZcGJoHeKal-RtyP8\ + PfvEAWTjSOCpBlSdUJ-1JL3tyd97uVFNaVuH3i7vvcMoUP_bdr0XW3rvRgaeC6X4daPLUvR1hK5Msut\ + QMtM2vpFghS_zZxIQRqz3B2ECxa9Bjxhmn8kLX5heZ8fq3lH-bmJp1DxzZ4V1RkWk.yVwXG9yARa5Ih\ + q2koh2NbQ + """) + + let data = Data(base64Encoded: devicePrivKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + let userKey = try JWEHelper.decryptUserKey(jwe: jwe, privateKey: privateKey) + + let x = userKey.x963Representation[1 ..< 49] + let y = userKey.x963Representation[49 ..< 97] + let k = userKey.x963Representation[97 ..< 145] + + /// PKSCS #8: MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDCi4K1Ts3DgTz/ufkLX7EGMHjGpJv+WJmFgyzLwwaDFSfLpDw0Kgf3FKK+LAsV8r+hZANiAARLOtFebIjxVYUmDV09Q1sVxz2Nm+NkR8fu6UojVSRcCW13tEZatx8XGrIY9zC7oBCEdRqDc68PMSvS5RA0Pg9cdBNc/kgMZ1iEmEv5YsqOcaNADDSs0bLlXb35pX7Kx5Y= + /// see: (crypto.spec.ts) in the Hub Frontend + XCTAssertEqual(x.base64URLEncodedString(), "SzrRXmyI8VWFJg1dPUNbFcc9jZvjZEfH7ulKI1UkXAltd7RGWrcfFxqyGPcwu6AQ") + XCTAssertEqual(y.base64URLEncodedString(), "hHUag3OvDzEr0uUQND4PXHQTXP5IDGdYhJhL-WLKjnGjQAw0rNGy5V29-aV-yseW") + XCTAssertEqual(k.base64URLEncodedString(), "wouCtU7Nw4E8_7n5C1-xBjB4xqSb_liZhYMsy8MGgxUny6Q8NCoH9xSiviwLFfK_") + } + + func testDecryptUserKeyECDHESWrongKey() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrZXlfb3BzIjpbXSwiZXh0Ijp\ + 0cnVlLCJrdHkiOiJFQyIsIngiOiJoeHpiSWh6SUJza3A5ZkZFUmJSQ2RfOU1fbWYxNElqaDZhcnNoVX\ + NkcEEyWno5ejZZNUs4NHpZR2I4b2FHemNUIiwieSI6ImJrMGRaNWhpelZ0TF9hN2hNejBjTUduNjhIR\ + jZFdWlyNHdlclNkTFV5QWd2NWUzVzNYSG5sdHJ2VlRyU3pzUWYiLCJjcnYiOiJQLTM4NCJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..pu3Q1nR_yvgRAapG.4zW0xm0JPxbcvZ66R-Mn3k841lHelDQfaUvsZZAtWs\ + L2w4FMi6H_uu6ArAWYLtNREa_zfcPuyuJsFferYPSNRUWt4OW6aWs-l_wfo7G1ceEVxztQXzQiwD30U\ + TA8OOdPcUuFfEq2-d9217jezrcyO6m6FjyssEZIrnRArUPWKzGdghXccGkkf0LTZcGJoHeKal-RtyP8\ + PfvEAWTjSOCpBlSdUJ-1JL3tyd97uVFNaVuH3i7vvcMoUP_bdr0XW3rvRgaeC6X4daPLUvR1hK5Msut\ + QMtM2vpFghS_zZxIQRqz3B2ECxa9Bjxhmn8kLX5heZ8fq3lH-bmJp1DxzZ4V1RkWk.yVwXG9yARa5Ih\ + q2koh2NbQ + """) + + let data = Data(base64Encoded: userPrivKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + + XCTAssertThrowsError(try JWEHelper.decryptUserKey(jwe: jwe, privateKey: privateKey)) { error in + guard case JOSESwiftError.decryptingFailed = error else { + XCTFail("Unexpected error: \(error)") + return + } + } + } + + func testDecryptUserKeyPBES2() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJzIjoiT3hMY0Q\ + xX1pCODc1c2hvUWY2Q1ZHQSIsInAyYyI6MTAwMCwiYXB1IjoiIiwiYXB2IjoiIn0.FD4fcrP4Pb\ + aKOQ9ZfXl0gpMM6Fa2rfqAvL0K5ZyYUiVeHCNV-A02Rg.urT1ShSv6qQxh8X7.gEqAiUWD98a2E\ + P7ITCPTw4DJo6-BpqrxA73D6gNIj9z4d1hN-EP99Q4mWBWLH97H8ugbG5rGsm8xsjsBqpWORQqF\ + mJZR2AhlPiwFaC7n_MDDBupSy_swDnCfj731Lal297IP5WbkFcmozKsyhmwdkctxjf_VHA.fJki\ + kDjUaxwUKqpvT7qaAQ + """) + + let userKey = try JWEHelper.decryptUserKey(jwe: jwe, setupCode: "123456") + + let x = userKey.x963Representation[1 ..< 49] + let y = userKey.x963Representation[49 ..< 97] + let k = userKey.x963Representation[97 ..< 145] + + /// PKSCS #8: ME8CAQAwEAYHKoZIzj0CAQYFK4EEACIEODA2AgEBBDEA6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ + /// see: (jwe.spec.ts) in the Hub Frontend + XCTAssertEqual(x.base64URLEncodedString(), "RxQR-NRN6Wga01370uBBzr2NHDbKIC56tPUEq2HX64RhITGhii8Zzbkb1HnRmdF0") + XCTAssertEqual(y.base64URLEncodedString(), "aq6uqmUy4jUhuxnKxsv59A6JeK7Unn-mpmm3pQAygjoGc9wrvoH4HWJSQYUlsXDu") + XCTAssertEqual(k.base64URLEncodedString(), "6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ") + } + + func testDecryptUserKeyPBES2WrongKey() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJzIjoiT3hMY0Q\ + xX1pCODc1c2hvUWY2Q1ZHQSIsInAyYyI6MTAwMCwiYXB1IjoiIiwiYXB2IjoiIn0.FD4fcrP4Pb\ + aKOQ9ZfXl0gpMM6Fa2rfqAvL0K5ZyYUiVeHCNV-A02Rg.urT1ShSv6qQxh8X7.gEqAiUWD98a2E\ + P7ITCPTw4DJo6-BpqrxA73D6gNIj9z4d1hN-EP99Q4mWBWLH97H8ugbG5rGsm8xsjsBqpWORQqF\ + mJZR2AhlPiwFaC7n_MDDBupSy_swDnCfj731Lal297IP5WbkFcmozKsyhmwdkctxjf_VHA.fJki\ + kDjUaxwUKqpvT7qaAQ + """) + + XCTAssertThrowsError(try JWEHelper.decryptUserKey(jwe: jwe, setupCode: "654321")) { error in + guard case JOSESwiftError.decryptingFailed = error else { + XCTFail("Unexpected error: \(error)") + return + } + } + } + + func testDecryptVaultKey() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlA\ + tMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IllUcEY3bGtTc3JvZVVUVFdCb21LNzBTN0\ + FhVTJyc0ptMURpZ1ZzbjRMY2F5eUxFNFBabldkYmFVcE9jQVV5a1ciLCJ5IjoiLU5pS3loUktjSk52N\ + m02Z0ZJUWc4cy1Xd1VXUW9uT3A5dkQ4cHpoa2tUU3U2RzFlU2FUTVlhZGltQ2Q4V0ExMSJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..BECWGzd9UvhHcTJC.znt4TlS-qiNEjxiu2v-du_E1QOBnyBR6LCt865SHxD\ + -kwRc1JwX_Lq9XVoFj2GnK9-9CgxhCLGurg5Jt9g38qv2brGAzWL7eSVeY1fIqdO_kUhLpGslRTN6h2\ + U0NHJi2-iE.WDVI2kOk9Dy3PWHyIg8gKA + """) + + let data = Data(base64Encoded: privKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + let masterkey = try JWEHelper.decryptVaultKey(jwe: jwe, with: privateKey) + + let expectedEncKey = [UInt8](repeating: 0x55, count: 32) + let expectedMacKey = [UInt8](repeating: 0x77, count: 32) + + XCTAssertEqual(masterkey.aesMasterKey, expectedEncKey) + XCTAssertEqual(masterkey.macMasterKey, expectedMacKey) + } + + func testDecryptInvalidVaultKey_wrongKey() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImdodGR3VnNoUU8wRGFBdjVBOXBiZ1NCTW0yYzZKWVF4dkloR3p6RVdQTncxczZZcEFYeTRQTjBXRFJUWExtQ2wiLCJ5IjoiN3Rncm1Gd016NGl0ZmVQNzBndkpLcjRSaGdjdENCMEJHZjZjWE9WZ2M0bjVXMWQ4dFgxZ1RQakdrczNVSm1zUiJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..x6JWRGSojUJUJYpp.5BRuzcaV.lLIhGH7Wz0n_iTBAubDFZA + """) + + let data = Data(base64Encoded: privKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + + XCTAssertThrowsError(try JWEHelper.decryptVaultKey(jwe: jwe, with: privateKey)) { error in + guard case JOSESwiftError.decryptingFailed = error else { + XCTFail("Unexpected error: \(error)") + return + } + } + } + + func testDecryptInvalidVaultKey_payloadIsNotJSON() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IkM2bWhsNE5BTHhEdHMwUlFlNXlyZWxQVDQyOGhDVzJNeUNYS3EwdUI0TDFMdnpXRHhVaVk3YTdZcEhJakJXcVoiLCJ5IjoiakM2dWc1NE9tbmdpNE9jUk1hdkNrczJpcFpXQjdkUmotR3QzOFhPSDRwZ2tpQ0lybWNlUnFxTnU3Z0c3Qk1yOSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..HNJJghL-SvERFz2v.N0z8YwFg.rYw29iX4i8XujdM4P4KKWg + """) + + let data = Data(base64Encoded: privKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + + XCTAssertThrowsError(try JWEHelper.decryptVaultKey(jwe: jwe, with: privateKey)) { error in + guard case DecodingError.dataCorrupted = error else { + XCTFail("Unexpected error: \(error)") + return + } + } + } + + func testDecryptInvalidVaultKey_jsonDoesNotContainKey() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6InB3R05vcXRnY093MkJ6RDVmSnpBWDJvMzUwSWNsY3A5cFdVTHZ5VDRqRWVCRWdCc3hhTVJXQ1ZyNlJMVUVXVlMiLCJ5IjoiZ2lIVEE5MlF3VU5lbmg1OFV1bWFfb09BX3hnYmFDVWFXSlRnb3Z4WjU4R212TnN4eUlQRElLSm9WV1h5X0R6OSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..jDbzdI7d67_cUjGD.01BPnMq_tQ.aG_uFA6FYqoPS64QAJ4VBQ + """) + + let data = Data(base64Encoded: privKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + + XCTAssertThrowsError(try JWEHelper.decryptVaultKey(jwe: jwe, with: privateKey)) { error in + guard case DecodingError.keyNotFound = error else { + XCTFail("Unexpected error: \(error)") + return + } + } + } + + func testDecryptInvalidVaultKey_jsonKeyIsNotAString() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IkJyYm9UQkl5Y0NDUEdJQlBUekU2RjBnbTRzRjRCamZPN1I0a2x0aWlCaThKZkxxcVdXNVdUSVBLN01yMXV5QVUiLCJ5IjoiNUpGVUI0WVJiYjM2RUZpN2Y0TUxMcFFyZXd2UV9Tc3dKNHRVbFd1a2c1ZU04X1ZyM2pkeml2QXI2WThRczVYbSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..QEq4Z2m6iwBx2ioS.IBo8TbKJTS4pug.61Z-agIIXgP8bX10O_yEMA + """) + + let data = Data(base64Encoded: privKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + + XCTAssertThrowsError(try JWEHelper.decryptVaultKey(jwe: jwe, with: privateKey)) { error in + guard case DecodingError.typeMismatch = error else { + XCTFail("Unexpected error: \(error)") + return + } + } + } + + func testDecryptInvalidVaultKey_invalidBase64Data() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImNZdlVFZm9LYkJjenZySE5zQjUxOGpycUxPMGJDOW5lZjR4NzFFMUQ5dk95MXRqd1piZzV3cFI0OE5nU1RQdHgiLCJ5IjoiaWRJekhCWERzSzR2NTZEeU9yczJOcDZsSG1zb29fMXV0VTlzX3JNdVVkbkxuVXIzUXdLZkhYMWdaVXREM1RKayJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..0VZqu5ei9U3blGtq.eDvhU6drw7mIwvXu6Q.f05QnhI7JWG3IYHvexwdFQ + """) + + let data = Data(base64Encoded: privKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + + XCTAssertThrowsError(try JWEHelper.decryptVaultKey(jwe: jwe, with: privateKey)) { error in + guard case JWEHelperError.invalidMasterkeyPayload = error else { + XCTFail("Unexpected error: \(error)") + return + } + } + } +} diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderAccountManagerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderAccountManagerTests.swift index c25acb343..ce7b48842 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderAccountManagerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderAccountManagerTests.swift @@ -10,27 +10,13 @@ import Foundation import GRDB import XCTest @testable import CryptomatorCommonCore +@testable import Dependencies class CloudProviderAccountManagerTests: XCTestCase { var accountManager: CloudProviderAccountDBManager! - var tmpDir: URL! override func setUpWithError() throws { - tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true, attributes: nil) - let dbPool = try DatabasePool(path: tmpDir.appendingPathComponent("db.sqlite").path) - try dbPool.write { db in - try db.create(table: CloudProviderAccount.databaseTableName) { table in - table.column(CloudProviderAccount.accountUIDKey, .text).primaryKey() - table.column(CloudProviderAccount.cloudProviderTypeKey, .text).notNull() - } - } - accountManager = CloudProviderAccountDBManager(dbPool: dbPool) - } - - override func tearDownWithError() throws { - accountManager = nil - try FileManager.default.removeItem(at: tmpDir) + accountManager = CloudProviderAccountDBManager() } func testSaveAccount() throws { diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderManagerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderManagerTests.swift index 782e06ba9..be08fd73c 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderManagerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderManagerTests.swift @@ -15,25 +15,12 @@ import XCTest class CloudProviderManagerTests: XCTestCase { var manager: CloudProviderDBManager! var accountManager: CloudProviderAccountDBManager! - var tmpDir: URL! + override func setUpWithError() throws { - tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true, attributes: nil) - let dbPool = try DatabasePool(path: tmpDir.appendingPathComponent("db.sqlite").path) - try dbPool.write { db in - try db.create(table: CloudProviderAccount.databaseTableName) { table in - table.column(CloudProviderAccount.accountUIDKey, .text).primaryKey() - table.column(CloudProviderAccount.cloudProviderTypeKey, .text).notNull() - } - } - accountManager = CloudProviderAccountDBManager(dbPool: dbPool) + accountManager = CloudProviderAccountDBManager() manager = CloudProviderDBManager(accountManager: accountManager) } - override func tearDownWithError() throws { - try FileManager.default.removeItem(at: tmpDir) - } - func testCreateProviderCachesTheProvider() throws { DropboxSetup.constants = DropboxSetup(appKey: "", sharedContainerIdentifier: nil, keychainService: nil, forceForegroundSession: false) let account = CloudProviderAccount(accountUID: UUID().uuidString, cloudProviderType: .dropbox) diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/S3CredentialManagerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/S3CredentialManagerTests.swift index 1cc629adf..8012a5806 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/S3CredentialManagerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/S3CredentialManagerTests.swift @@ -16,13 +16,8 @@ class S3CredentialManagerTests: XCTestCase { let displayName = "Cryptomator S3" override func setUpWithError() throws { - var configuration = Configuration() - // Workaround for a SQLite regression (see https://github.com/groue/GRDB.swift/issues/1171 for more details) - configuration.acceptsDoubleQuotedStringLiterals = true - let inMemoryDB = DatabaseQueue(configuration: configuration) - try CryptomatorDatabase.migrator.migrate(inMemoryDB) cryptomatorKeychainMock = CryptomatorKeychainMock() - manager = S3CredentialManager(dbWriter: inMemoryDB, keychain: cryptomatorKeychainMock) + manager = S3CredentialManager(keychain: cryptomatorKeychainMock) } func testSaveCredential() throws { diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultDBCacheTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultDBCacheTests.swift index 78f2de195..5a47c5185 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultDBCacheTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultDBCacheTests.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Skymatic GmbH. All rights reserved. // +import Dependencies import Foundation import GRDB import Promises @@ -21,7 +22,6 @@ class VaultDBCacheTests: XCTestCase { private let vaultPath = CloudPath("/Vault") private lazy var vaultAccount: VaultAccount = .init(vaultUID: vaultUID, delegateAccountUID: account.accountUID, vaultPath: vaultPath, vaultName: "Vault") private let cloudProviderMock = CloudProviderMock() - private var inMemoryDB: DatabaseQueue! private var masterkeyFileData: Data! private var updatedMasterkeyFileData: Data! private let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) @@ -40,16 +40,9 @@ class VaultDBCacheTests: XCTestCase { vaultConfigData = try vaultConfig.toToken(keyId: "masterkeyfile:masterkey.cryptomator", rawKey: masterkey.rawKey) updatedVaultConfigData = try vaultConfig.toToken(keyId: "masterkeyfile:masterkey.cryptomator", rawKey: updatedMasterkey.rawKey) defaultCachedVault = CachedVault(vaultUID: vaultUID, masterkeyFileData: masterkeyFileData, vaultConfigToken: vaultConfigData, lastUpToDateCheck: Date(timeIntervalSince1970: 0), masterkeyFileLastModifiedDate: Date(timeIntervalSince1970: 0), vaultConfigLastModifiedDate: Date(timeIntervalSince1970: 0)) - var configuration = Configuration() - // Workaround for a SQLite regression (see https://github.com/groue/GRDB.swift/issues/1171 for more details) - configuration.acceptsDoubleQuotedStringLiterals = true - inMemoryDB = DatabaseQueue(configuration: configuration) - vaultCache = VaultDBCache(dbWriter: inMemoryDB) - try CryptomatorDatabase.migrator.migrate(inMemoryDB) - try inMemoryDB.write { db in - try account.save(db) - try vaultAccount.save(db) - } + + vaultCache = VaultDBCache() + try prepareDatabase() } func testCacheVault() throws { @@ -74,9 +67,11 @@ class VaultDBCacheTests: XCTestCase { } func testCascadeOnVaultAccountDeletion() throws { + @Dependency(\.database) var database + try vaultCache.cache(defaultCachedVault) - _ = try inMemoryDB.write { db in + _ = try database.write { db in try vaultAccount.delete(db) } XCTAssertThrowsError(try vaultCache.getCachedVault(withVaultUID: vaultUID)) { error in @@ -304,4 +299,12 @@ class VaultDBCacheTests: XCTestCase { private func assertDownloadedOnlyMasterkey() { XCTAssertEqual([CloudPath("/Vault/masterkey.cryptomator")], cloudProviderMock.downloadFileFromToReceivedInvocations.map { $0.cloudPath }) } + + private func prepareDatabase() throws { + @Dependency(\.database) var database + try database.write { db in + try account.save(db) + try vaultAccount.save(db) + } + } } diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift index bd12cf3cc..a4a00b0fe 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift @@ -15,7 +15,7 @@ import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorCryptoLib -class VaultManagerMock: VaultDBManager { +private final class VaultManagerMock: VaultDBManager { var removedVaultUIDs = [String]() var addedFileProviderDomainDisplayName = [String: String]() @@ -61,8 +61,6 @@ class VaultManagerTests: XCTestCase { var masterkeyCacheManagerMock: MasterkeyCacheManagerMock! var masterkeyCacheHelperMock: MasterkeyCacheHelperMock! var cloudProviderMock: CloudProviderMock! - var tmpDir: URL! - var dbPool: DatabasePool! let vaultUID = "VaultUID-12345" let passphrase = "PW" let delegateAccountUID = UUID().uuidString @@ -74,17 +72,10 @@ class VaultManagerTests: XCTestCase { override func setUpWithError() throws { cloudProviderMock = CloudProviderMock() - tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true, attributes: nil) - var configuration = Configuration() - // Workaround for a SQLite regression (see https://github.com/groue/GRDB.swift/issues/1171 for more details) - configuration.acceptsDoubleQuotedStringLiterals = true - dbPool = try DatabasePool(path: tmpDir.appendingPathComponent("db.sqlite").path, configuration: configuration) - try CryptomatorDatabase.migrator.migrate(dbPool) - - providerAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) + + providerAccountManager = CloudProviderAccountDBManager() providerManager = CloudProviderManagerMock(provider: cloudProviderMock, accountManager: providerAccountManager) - accountManager = VaultAccountDBManager(dbPool: dbPool) + accountManager = VaultAccountDBManager() vaultCacheMock = VaultCacheMock() vaultCacheMock.refreshVaultCacheForWithReturnValue = Promise(()) passwordManagerMock = VaultPasswordManagerMock() @@ -94,16 +85,6 @@ class VaultManagerTests: XCTestCase { manager = VaultManagerMock(providerManager: providerManager, vaultAccountManager: accountManager, vaultCache: vaultCacheMock, passwordManager: passwordManagerMock, masterkeyCacheManager: masterkeyCacheManagerMock, masterkeyCacheHelper: masterkeyCacheHelperMock) } - override func tearDownWithError() throws { - // Set all objects related to the sqlite database to nil to avoid warnings about database integrity when deleting the test database. - manager = nil - providerAccountManager = nil - providerManager = nil - accountManager = nil - dbPool = nil - try FileManager.default.removeItem(at: tmpDir) - } - func testCreateNewVault() throws { let expectation = XCTestExpectation() let delegateAccountUID = UUID().uuidString @@ -144,11 +125,11 @@ class VaultManagerTests: XCTestCase { } XCTAssertEqual(uploadedVaultConfigToken, savedVaultConfigToken) let vaultConfig = try UnverifiedVaultConfig(token: savedVaultConfigToken) - XCTAssertEqual("SIV_CTRMAC", vaultConfig.allegedCipherCombo) + XCTAssertEqual("SIV_GCM", vaultConfig.allegedCipherCombo) XCTAssertEqual(8, vaultConfig.allegedFormat) let uploadedRootDirIdFile = try getUploadedData(at: CloudPath(cloudProviderMock.createdFolders[3]).appendingPathComponent("dirid.c9r")) - XCTAssertEqual(88, uploadedRootDirIdFile.count) + XCTAssertEqual(68, uploadedRootDirIdFile.count) }.catch { error in XCTFail("Promise failed with error: \(error)") }.always { diff --git a/CryptomatorFileProvider/DB/WorkingSetObserver.swift b/CryptomatorFileProvider/DB/WorkingSetObserver.swift index 8b35d1d62..b4f411807 100644 --- a/CryptomatorFileProvider/DB/WorkingSetObserver.swift +++ b/CryptomatorFileProvider/DB/WorkingSetObserver.swift @@ -7,6 +7,7 @@ // import CocoaLumberjackSwift +import Dependencies import FileProvider import Foundation import GRDB @@ -23,8 +24,13 @@ class WorkingSetObserver: WorkingSetObserving { private let notificator: FileProviderNotificatorType private var currentWorkingSetItems = Set() private let domainIdentifier: NSFileProviderDomainIdentifier + @Dependency(\.permissionProvider) private var permissionProvider - init(domainIdentifier: NSFileProviderDomainIdentifier, database: DatabaseReader, notificator: FileProviderNotificatorType, uploadTaskManager: UploadTaskManager, cachedFileManager: CachedFileManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + database: DatabaseReader, + notificator: FileProviderNotificatorType, + uploadTaskManager: UploadTaskManager, + cachedFileManager: CachedFileManager) { self.domainIdentifier = domainIdentifier self.database = database self.notificator = notificator diff --git a/CryptomatorFileProvider/DatabaseURLProvider.swift b/CryptomatorFileProvider/DatabaseURLProvider.swift index 987b7d22c..554d9a1b1 100644 --- a/CryptomatorFileProvider/DatabaseURLProvider.swift +++ b/CryptomatorFileProvider/DatabaseURLProvider.swift @@ -13,10 +13,6 @@ public struct DatabaseURLProvider { public static let shared = DatabaseURLProvider(documentStorageURLProvider: NSFileProviderManager.default) let documentStorageURLProvider: DocumentStorageURLProvider - init(documentStorageURLProvider: DocumentStorageURLProvider) { - self.documentStorageURLProvider = documentStorageURLProvider - } - public func getDatabaseURL(for domain: NSFileProviderDomain) -> URL { let documentStorageURL = documentStorageURLProvider.documentStorageURL let domainURL = documentStorageURL.appendingPathComponent(domain.pathRelativeToDocumentStorage, isDirectory: true) diff --git a/CryptomatorFileProvider/FileProviderAdapter.swift b/CryptomatorFileProvider/FileProviderAdapter.swift index 05fab94dd..2ff7e112c 100644 --- a/CryptomatorFileProvider/FileProviderAdapter.swift +++ b/CryptomatorFileProvider/FileProviderAdapter.swift @@ -9,6 +9,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import Promises @@ -68,11 +69,12 @@ public class FileProviderAdapter: FileProviderAdapterType { private let provider: CloudProvider private let localURLProvider: LocalURLProviderType private let notificator: FileProviderItemUpdateDelegate? - private let fullVersionChecker: FullVersionChecker + @Dependency(\.fullVersionChecker) private var fullVersionChecker private let workflowFactory: WorkflowFactoryLocking private let domainIdentifier: NSFileProviderDomainIdentifier private let fileCoordinator: NSFileCoordinator private let taskRegistrator: SessionTaskRegistrator + @Dependency(\.permissionProvider) private var permissionProvider init(domainIdentifier: NSFileProviderDomainIdentifier, uploadTaskManager: UploadTaskManager, @@ -87,7 +89,6 @@ public class FileProviderAdapter: FileProviderAdapterType { coordinator: NSFileCoordinator, notificator: FileProviderItemUpdateDelegate? = nil, localURLProvider: LocalURLProviderType, - fullVersionChecker: FullVersionChecker = GlobalFullVersionChecker.default, taskRegistrator: SessionTaskRegistrator) { self.lastUnlockedDate = Date() self.domainIdentifier = domainIdentifier @@ -112,7 +113,6 @@ public class FileProviderAdapter: FileProviderAdapterType { self.provider = provider self.notificator = notificator self.localURLProvider = localURLProvider - self.fullVersionChecker = fullVersionChecker self.fileCoordinator = coordinator self.taskRegistrator = taskRegistrator } diff --git a/CryptomatorFileProvider/FileProviderAdapterError.swift b/CryptomatorFileProvider/FileProviderAdapterError.swift index 3e427f9a1..a2e23434f 100644 --- a/CryptomatorFileProvider/FileProviderAdapterError.swift +++ b/CryptomatorFileProvider/FileProviderAdapterError.swift @@ -1,5 +1,5 @@ // -// FileProviderDecoratorError.swift +// FileProviderAdapterError.swift // CryptomatorFileProvider // // Created by Philipp Schmid on 24.06.20. diff --git a/CryptomatorFileProvider/FileProviderAdapterManager.swift b/CryptomatorFileProvider/FileProviderAdapterManager.swift index 3a374fef3..d53e08185 100644 --- a/CryptomatorFileProvider/FileProviderAdapterManager.swift +++ b/CryptomatorFileProvider/FileProviderAdapterManager.swift @@ -9,6 +9,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import GRDB @@ -32,12 +33,27 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { private let notificatorManager: FileProviderNotificatorManagerType private let queue = DispatchQueue(label: "FileProviderAdapterManager", qos: .userInitiated) private let providerIdentifier: String + @Dependency(\.permissionProvider) private var permissionProvider convenience init() { - self.init(masterkeyCacheManager: MasterkeyCacheKeychainManager.shared, vaultKeepUnlockedHelper: VaultKeepUnlockedManager.shared, vaultKeepUnlockedSettings: VaultKeepUnlockedManager.shared, vaultManager: VaultDBManager.shared, adapterCache: FileProviderAdapterCache(), notificatorManager: FileProviderNotificatorManager.shared, unlockMonitor: UnlockMonitor(), providerIdentifier: NSFileProviderManager.default.providerIdentifier) + self.init(masterkeyCacheManager: MasterkeyCacheKeychainManager.shared, + vaultKeepUnlockedHelper: VaultKeepUnlockedManager.shared, + vaultKeepUnlockedSettings: VaultKeepUnlockedManager.shared, + vaultManager: VaultDBManager.shared, + adapterCache: FileProviderAdapterCache(), + notificatorManager: FileProviderNotificatorManager.shared, + unlockMonitor: UnlockMonitor(), + providerIdentifier: NSFileProviderManager.default.providerIdentifier) } - init(masterkeyCacheManager: MasterkeyCacheManager, vaultKeepUnlockedHelper: VaultKeepUnlockedHelper, vaultKeepUnlockedSettings: VaultKeepUnlockedSettings, vaultManager: VaultManager, adapterCache: FileProviderAdapterCacheType, notificatorManager: FileProviderNotificatorManagerType, unlockMonitor: UnlockMonitorType, providerIdentifier: String) { + init(masterkeyCacheManager: MasterkeyCacheManager, + vaultKeepUnlockedHelper: VaultKeepUnlockedHelper, + vaultKeepUnlockedSettings: VaultKeepUnlockedSettings, + vaultManager: VaultManager, + adapterCache: FileProviderAdapterCacheType, + notificatorManager: FileProviderNotificatorManagerType, + unlockMonitor: UnlockMonitorType, + providerIdentifier: String) { self.masterkeyCacheManager = masterkeyCacheManager self.vaultKeepUnlockedHelper = vaultKeepUnlockedHelper self.vaultKeepUnlockedSettings = vaultKeepUnlockedSettings @@ -81,6 +97,30 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { return } let provider = try vaultManager.manualUnlockVault(withUID: domainIdentifier.rawValue, kek: kek) + try unlockVaultPostProcessing(provider: provider, + domainIdentifier: domainIdentifier, + dbPath: dbPath, + delegate: delegate, + notificator: notificator, + taskRegistrator: taskRegistrator) + } + + // swiftlint:disable:next function_parameter_count + public func unlockVault(with domainIdentifier: NSFileProviderDomainIdentifier, rawKey: [UInt8], dbPath: URL?, delegate: FileProviderAdapterDelegate, notificator: FileProviderNotificatorType, taskRegistrator: SessionTaskRegistrator) throws { + guard let dbPath = dbPath else { + return + } + let provider = try vaultManager.manualUnlockVault(withUID: domainIdentifier.rawValue, rawKey: rawKey) + try unlockVaultPostProcessing(provider: provider, + domainIdentifier: domainIdentifier, + dbPath: dbPath, + delegate: delegate, + notificator: notificator, + taskRegistrator: taskRegistrator) + } + + // swiftlint:disable:next function_parameter_count + func unlockVaultPostProcessing(provider: CloudProvider, domainIdentifier: NSFileProviderDomainIdentifier, dbPath: URL, delegate: FileProviderAdapterDelegate, notificator: FileProviderNotificatorType, taskRegistrator: SessionTaskRegistrator) throws { let item = try createAdapterCacheItem(domainIdentifier: domainIdentifier, cloudProvider: provider, dbPath: dbPath, delegate: delegate, notificator: notificator, taskRegistrator: taskRegistrator) try vaultKeepUnlockedSettings.setLastUsedDate(Date(), forVaultUID: domainIdentifier.rawValue) adapterCache.cacheItem(item, identifier: domainIdentifier) @@ -166,7 +206,12 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { notificator: notificator, localURLProvider: delegate, taskRegistrator: taskRegistrator) - let workingSetObserver = WorkingSetObserver(domainIdentifier: domainIdentifier, database: database, notificator: notificator, uploadTaskManager: uploadTaskManager, cachedFileManager: cachedFileManager) + + let workingSetObserver = WorkingSetObserver(domainIdentifier: domainIdentifier, + database: database, + notificator: notificator, + uploadTaskManager: uploadTaskManager, + cachedFileManager: cachedFileManager) workingSetObserver.startObservation() return AdapterCacheItem(adapter: adapter, maintenanceManager: maintenanceManager, workingSetObserver: workingSetObserver) } diff --git a/CryptomatorFileProvider/FileProviderItem.swift b/CryptomatorFileProvider/FileProviderItem.swift index 6d67274a3..2b0bc5955 100644 --- a/CryptomatorFileProvider/FileProviderItem.swift +++ b/CryptomatorFileProvider/FileProviderItem.swift @@ -8,6 +8,7 @@ import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import MobileCoreServices @@ -22,15 +23,15 @@ public class FileProviderItem: NSObject, NSFileProviderItem { let newestVersionLocallyCached: Bool let localURL: URL? let domainIdentifier: NSFileProviderDomainIdentifier - private let fullVersionChecker: FullVersionChecker + @Dependency(\.fullVersionChecker) private var fullVersionChecker + @Dependency(\.permissionProvider) private var permissionProvider - init(metadata: ItemMetadata, domainIdentifier: NSFileProviderDomainIdentifier, newestVersionLocallyCached: Bool = false, localURL: URL? = nil, error: Error? = nil, fullVersionChecker: FullVersionChecker = GlobalFullVersionChecker.default) { + init(metadata: ItemMetadata, domainIdentifier: NSFileProviderDomainIdentifier, newestVersionLocallyCached: Bool = false, localURL: URL? = nil, error: Error? = nil) { self.metadata = metadata self.domainIdentifier = domainIdentifier self.error = error self.newestVersionLocallyCached = newestVersionLocallyCached self.localURL = localURL - self.fullVersionChecker = fullVersionChecker } public var itemIdentifier: NSFileProviderItemIdentifier { @@ -50,19 +51,7 @@ public class FileProviderItem: NSObject, NSFileProviderItem { } public var capabilities: NSFileProviderItemCapabilities { - if metadata.statusCode == .uploadError { - return .allowsDeleting - } - if !fullVersionChecker.isFullVersion { - return FileProviderItem.readOnlyCapabilities - } - if metadata.type == .folder { - return [.allowsAddingSubItems, .allowsContentEnumerating, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] - } - if metadata.statusCode == .isUploading { - return .allowsReading - } - return [.allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] + return permissionProvider.getPermissions(for: metadata, at: domainIdentifier) } public var filename: String { diff --git a/CryptomatorFileProvider/FileProviderItemList.swift b/CryptomatorFileProvider/FileProviderItemList.swift index fb728d004..5530b2015 100644 --- a/CryptomatorFileProvider/FileProviderItemList.swift +++ b/CryptomatorFileProvider/FileProviderItemList.swift @@ -12,9 +12,4 @@ import Foundation public struct FileProviderItemList { public let items: [FileProviderItem] public let nextPageToken: NSFileProviderPage? - - init(items: [FileProviderItem], nextPageToken: NSFileProviderPage?) { - self.items = items - self.nextPageToken = nextPageToken - } } diff --git a/CryptomatorFileProvider/LocalURLProviderType.swift b/CryptomatorFileProvider/LocalURLProviderType.swift index 653dcc0f5..5295b3795 100644 --- a/CryptomatorFileProvider/LocalURLProviderType.swift +++ b/CryptomatorFileProvider/LocalURLProviderType.swift @@ -1,5 +1,5 @@ // -// LocalURLProvider.swift +// LocalURLProviderType.swift // CryptomatorFileProvider // // Created by Philipp Schmid on 03.03.22. diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift index c3feed4e7..6e1c23446 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift @@ -8,6 +8,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore +import Dependencies import Foundation import Promises @@ -30,8 +31,13 @@ class DownloadTaskExecutor: WorkflowMiddleware { private let downloadTaskManager: DownloadTaskManager private let provider: CloudProvider private let domainIdentifier: NSFileProviderDomainIdentifier + @Dependency(\.permissionProvider) private var permissionProvider - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager, downloadTaskManager: DownloadTaskManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + itemMetadataManager: ItemMetadataManager, + cachedFileManager: CachedFileManager, + downloadTaskManager: DownloadTaskManager) { self.domainIdentifier = domainIdentifier self.provider = provider self.itemMetadataManager = itemMetadataManager diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift index 23235e3b6..fd2623508 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift @@ -29,7 +29,9 @@ class FolderCreationTaskExecutor: WorkflowMiddleware { private let provider: CloudProvider private let domainIdentifier: NSFileProviderDomainIdentifier - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, itemMetadataManager: ItemMetadataManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + itemMetadataManager: ItemMetadataManager) { self.domainIdentifier = domainIdentifier self.provider = provider self.itemMetadataManager = itemMetadataManager @@ -53,11 +55,13 @@ class FolderCreationTaskExecutor: WorkflowMiddleware { assert(itemMetadata.id != nil) assert(itemMetadata.type == .folder) - return provider.createFolder(at: itemMetadata.cloudPath).then { _ -> FileProviderItem in + return provider.createFolder(at: itemMetadata.cloudPath).then { [domainIdentifier, itemMetadataManager] _ -> FileProviderItem in itemMetadata.statusCode = .isUploaded itemMetadata.isPlaceholderItem = false - try self.itemMetadataManager.updateMetadata(itemMetadata) - return FileProviderItem(metadata: itemMetadata, domainIdentifier: self.domainIdentifier, newestVersionLocallyCached: true) + try itemMetadataManager.updateMetadata(itemMetadata) + return FileProviderItem(metadata: itemMetadata, + domainIdentifier: domainIdentifier, + newestVersionLocallyCached: true) } } } diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift index db91a48d7..5d4e96be5 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift @@ -37,7 +37,15 @@ class ItemEnumerationTaskExecutor: WorkflowMiddleware { private let provider: CloudProvider private let domainIdentifier: NSFileProviderDomainIdentifier - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager, uploadTaskManager: UploadTaskManager, reparentTaskManager: ReparentTaskManager, deletionTaskManager: DeletionTaskManager, itemEnumerationTaskManager: ItemEnumerationTaskManager, deleteItemHelper: DeleteItemHelper) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + itemMetadataManager: ItemMetadataManager, + cachedFileManager: CachedFileManager, + uploadTaskManager: UploadTaskManager, + reparentTaskManager: ReparentTaskManager, + deletionTaskManager: DeletionTaskManager, + itemEnumerationTaskManager: ItemEnumerationTaskManager, + deleteItemHelper: DeleteItemHelper) { self.domainIdentifier = domainIdentifier self.provider = provider self.itemMetadataManager = itemMetadataManager diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift index 6593c372a..7b85d9a3c 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import Dependencies import FileProvider import Foundation import Promises @@ -30,8 +31,13 @@ class ReparentTaskExecutor: WorkflowMiddleware { private let itemMetadataManager: ItemMetadataManager private let cachedFileManager: CachedFileManager private let domainIdentifier: NSFileProviderDomainIdentifier + @Dependency(\.permissionProvider) private var permissionProvider - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, reparentTaskManager: ReparentTaskManager, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + reparentTaskManager: ReparentTaskManager, + itemMetadataManager: ItemMetadataManager, + cachedFileManager: CachedFileManager) { self.domainIdentifier = domainIdentifier self.provider = provider self.reparentTaskManager = reparentTaskManager diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift index a670cc153..2fc5abf38 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift @@ -8,6 +8,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore +import Dependencies import FileProvider import Foundation import Promises @@ -32,8 +33,14 @@ class UploadTaskExecutor: WorkflowMiddleware { let uploadTaskManager: UploadTaskManager let domainIdentifier: NSFileProviderDomainIdentifier let progressManager: ProgressManager + @Dependency(\.permissionProvider) private var permissionProvider - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, cachedFileManager: CachedFileManager, itemMetadataManager: ItemMetadataManager, uploadTaskManager: UploadTaskManager, progressManager: ProgressManager = InMemoryProgressManager.shared) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + cachedFileManager: CachedFileManager, + itemMetadataManager: ItemMetadataManager, + uploadTaskManager: UploadTaskManager, + progressManager: ProgressManager = InMemoryProgressManager.shared) { self.domainIdentifier = domainIdentifier self.provider = provider self.cachedFileManager = cachedFileManager diff --git a/CryptomatorFileProvider/PermissionProvider.swift b/CryptomatorFileProvider/PermissionProvider.swift new file mode 100644 index 000000000..bcdbee887 --- /dev/null +++ b/CryptomatorFileProvider/PermissionProvider.swift @@ -0,0 +1,127 @@ +// +// PermissionProvider.swift +// CryptomatorFileProvider +// +// Created by Philipp Schmid on 18.09.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import CocoaLumberjackSwift +import CryptomatorCommonCore +import Dependencies +import FileProvider +import Foundation + +public protocol PermissionProvider { + /** + Returns the permission for a given `item` at a given `domain`. + + The following restrictions can apply to any item: + - in case of an upload error it's only allowed to delete the item. + - in case of a free version only reading is allowed, except if the vault belongs to Cryptomator Hub and it has an active subscription state. + + The following capabilities hold for files: + - reading + - adding sub items + - content enumerating + - deleting + - renaming + - reparenting + + - Note: In case of an running upload, i.e. a creation of the folder in the cloud, the capabilities do not get restricted except if something listed above restricts all items of the vault. + + The following capabilities hold for files: + - reading + - writing + - deleting + - renaming + - reparenting + - Note: In case of an running upload for a file it's only allowed to read the item. To prevent additional modifications. + + */ + func getPermissions(for item: ItemMetadata, at domain: NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities + + func getPermissionsForRootItem(at domain: NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities +} + +private enum PermissionProviderKey: DependencyKey { + static let liveValue: PermissionProvider = PermissionProviderImpl() + #if DEBUG + static let testValue: PermissionProvider = UnimplementedPermissionProvider() + #endif +} + +extension DependencyValues { + var permissionProvider: PermissionProvider { + get { self[PermissionProviderKey.self] } + set { self[PermissionProviderKey.self] = newValue } + } +} + +struct PermissionProviderImpl: PermissionProvider { + @Dependency(\.fullVersionChecker) private var fullVersionChecker + @Dependency(\.hubRepository) private var hubRepository + + func getPermissions(for item: ItemMetadata, at domain: NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities { + if item.statusCode == .uploadError { + return .allowsDeleting + } + + let vaultID = domain.rawValue + let hubSubscriptionState: HubSubscriptionState? + do { + let hubVault = try hubRepository.getHubVault(vaultID: vaultID) + hubSubscriptionState = hubVault?.subscriptionState + } catch { + hubSubscriptionState = nil + DDLogError("Failed to retrieve possible hub vault for with id: \(vaultID)") + } + + if !fullVersionChecker.isFullVersion && hubSubscriptionState != .active { + return FileProviderItem.readOnlyCapabilities + } + if item.type == .folder { + return [.allowsAddingSubItems, .allowsContentEnumerating, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] + } + if item.statusCode == .isUploading { + return FileProviderItem.readOnlyCapabilities + } + return [.allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] + } + + func getPermissionsForRootItem(at domain: NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities { + if fullVersionChecker.isFullVersion { + return [.allowsAll] + } + guard let domain else { + return FileProviderItem.readOnlyCapabilities + } + let vaultID = domain.rawValue + let hubSubscriptionState: HubSubscriptionState? + do { + let hubVault = try hubRepository.getHubVault(vaultID: vaultID) + hubSubscriptionState = hubVault?.subscriptionState + } catch { + hubSubscriptionState = nil + DDLogError("Failed to retrieve possible hub vault for with id: \(vaultID)") + } + switch hubSubscriptionState { + case .active: + return [.allowsAll] + case .inactive, nil: + return FileProviderItem.readOnlyCapabilities + } + } +} + +#if DEBUG +struct UnimplementedPermissionProvider: PermissionProvider { + func getPermissions(for item: ItemMetadata, at domain: NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities { + unimplemented("\(Self.self).getPermissions", placeholder: .allowsReading) + } + + func getPermissionsForRootItem(at domain: NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities { + unimplemented("\(Self.self).getPermissionsForRootItem", placeholder: .allowsReading) + } +} +#endif diff --git a/CryptomatorFileProvider/Promise+AllIgnoringResult.swift b/CryptomatorFileProvider/Promise+AllIgnoringResult.swift index 23573f67b..057ba025a 100644 --- a/CryptomatorFileProvider/Promise+AllIgnoringResult.swift +++ b/CryptomatorFileProvider/Promise+AllIgnoringResult.swift @@ -1,5 +1,5 @@ // -// Promises+FinishedAll.swift +// Promise+AllIgnoringResult.swift // CryptomatorFileProvider // // Created by Philipp Schmid on 31.03.22. diff --git a/CryptomatorFileProvider/RootFileProviderItem.swift b/CryptomatorFileProvider/RootFileProviderItem.swift index 2f5c6fe56..fafb4dafb 100644 --- a/CryptomatorFileProvider/RootFileProviderItem.swift +++ b/CryptomatorFileProvider/RootFileProviderItem.swift @@ -7,6 +7,7 @@ // import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import MobileCoreServices @@ -18,19 +19,13 @@ public class RootFileProviderItem: NSObject, NSFileProviderItem { public let typeIdentifier = kUTTypeFolder as String public let documentSize: NSNumber? = nil public var capabilities: NSFileProviderItemCapabilities { - if fullVersionChecker.isFullVersion { - return [.allowsAll] - } else { - return FileProviderItem.readOnlyCapabilities - } + return permissionProvider.getPermissionsForRootItem(at: domain?.identifier) } - private let fullVersionChecker: FullVersionChecker - override public convenience init() { - self.init(fullVersionChecker: GlobalFullVersionChecker.default) - } + private let domain: NSFileProviderDomain? + @Dependency(\.permissionProvider) private var permissionProvider - init(fullVersionChecker: FullVersionChecker) { - self.fullVersionChecker = fullVersionChecker + public init(domain: NSFileProviderDomain?) { + self.domain = domain } } diff --git a/CryptomatorFileProvider/ServiceSource/FileImportingServiceSource.swift b/CryptomatorFileProvider/ServiceSource/FileImportingServiceSource.swift index a43596799..680a069ba 100644 --- a/CryptomatorFileProvider/ServiceSource/FileImportingServiceSource.swift +++ b/CryptomatorFileProvider/ServiceSource/FileImportingServiceSource.swift @@ -8,6 +8,7 @@ import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation @@ -17,7 +18,7 @@ public class FileImportingServiceSource: ServiceSource, FileImporting { private let dbPath: URL private let localURLProvider: LocalURLProviderType private let adapterManager: FileProviderAdapterProviding - private let fullVersionChecker: FullVersionChecker + @Dependency(\.fullVersionChecker) private var fullVersionChecker private let taskRegistrator: SessionTaskRegistrator public convenience init(domain: NSFileProviderDomain, notificator: FileProviderNotificatorType, dbPath: URL, delegate: LocalURLProviderType, taskRegistrator: SessionTaskRegistrator) { @@ -26,17 +27,15 @@ public class FileImportingServiceSource: ServiceSource, FileImporting { dbPath: dbPath, delegate: delegate, adapterManager: FileProviderAdapterManager.shared, - fullVersionChecker: GlobalFullVersionChecker.default, taskRegistrator: taskRegistrator) } - init(domain: NSFileProviderDomain, notificator: FileProviderNotificatorType, dbPath: URL, delegate: LocalURLProviderType, adapterManager: FileProviderAdapterProviding, fullVersionChecker: FullVersionChecker, taskRegistrator: SessionTaskRegistrator) { + init(domain: NSFileProviderDomain, notificator: FileProviderNotificatorType, dbPath: URL, delegate: LocalURLProviderType, adapterManager: FileProviderAdapterProviding, taskRegistrator: SessionTaskRegistrator) { self.domain = domain self.notificator = notificator self.dbPath = dbPath self.localURLProvider = delegate self.adapterManager = adapterManager - self.fullVersionChecker = fullVersionChecker self.taskRegistrator = taskRegistrator super.init(serviceName: .fileImporting, exportedInterface: NSXPCInterface(with: FileImporting.self)) } diff --git a/CryptomatorFileProvider/ServiceSource/VaultUnlockingServiceSource.swift b/CryptomatorFileProvider/ServiceSource/VaultUnlockingServiceSource.swift index c5103ac12..c80fd9d83 100644 --- a/CryptomatorFileProvider/ServiceSource/VaultUnlockingServiceSource.swift +++ b/CryptomatorFileProvider/ServiceSource/VaultUnlockingServiceSource.swift @@ -64,4 +64,26 @@ public class VaultUnlockingServiceSource: ServiceSource, VaultUnlocking { DDLogInfo("endBiometricalUnlock called for \(vaultUID)") FileProviderAdapterManager.shared.unlockMonitor.endBiometricalUnlock(forVaultUID: vaultUID) } + + public func unlockVault(rawKey: [UInt8], reply: @escaping (NSError?) -> Void) { + let domain = self.domain + let vaultUID = vaultUID + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + guard let notificator = self.notificator else { + DDLogError("Unlocking vault failed, unable to find FileProviderDomain") + reply(VaultManagerError.fileProviderDomainNotFound as NSError) + return + } + do { + try FileProviderAdapterManager.shared.unlockVault(with: domain.identifier, rawKey: rawKey, dbPath: self.dbPath, delegate: self.localURLProvider, notificator: notificator, taskRegistrator: self.taskRegistrator) + FileProviderAdapterManager.shared.unlockMonitor.unlockSucceeded(forVaultUID: vaultUID) + DDLogInfo("Unlocked vault \"\(domain.displayName)\" (\(domain.identifier.rawValue))") + reply(nil) + } catch { + FileProviderAdapterManager.shared.unlockMonitor.unlockFailed(forVaultUID: vaultUID) + DDLogError("Unlocking vault \"\(domain.displayName)\" (\(domain.identifier.rawValue)) failed with error: \(error)") + reply(XPCErrorHelper.bridgeError(error)) + } + } + } } diff --git a/CryptomatorFileProvider/Workflow/WorkflowFactory.swift b/CryptomatorFileProvider/Workflow/WorkflowFactory.swift index 2ebe1387e..17a92c359 100644 --- a/CryptomatorFileProvider/Workflow/WorkflowFactory.swift +++ b/CryptomatorFileProvider/Workflow/WorkflowFactory.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import Dependencies import FileProvider import Foundation @@ -21,6 +22,7 @@ struct WorkflowFactory { let downloadTaskManager: DownloadTaskManager let dependencyFactory = WorkflowDependencyFactory() let domainIdentifier: NSFileProviderDomainIdentifier + @Dependency(\.permissionProvider) private var permissionProvider func createWorkflow(for deletionTask: DeletionTask) -> Workflow { let taskExecutor = DeletionTaskExecutor(provider: provider, itemMetadataManager: itemMetadataManager) diff --git a/CryptomatorFileProviderTests/FileImportingServiceSourceTests.swift b/CryptomatorFileProviderTests/FileImportingServiceSourceTests.swift index f39f9fb59..37b28bcc5 100644 --- a/CryptomatorFileProviderTests/FileImportingServiceSourceTests.swift +++ b/CryptomatorFileProviderTests/FileImportingServiceSourceTests.swift @@ -10,6 +10,7 @@ import CryptomatorCloudAccessCore import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies @testable import Promises class FileImportingServiceSourceTests: XCTestCase { @@ -21,7 +22,7 @@ class FileImportingServiceSourceTests: XCTestCase { var taskRegistratorMock: SessionTaskRegistratorMock! let dbPath = FileManager.default.temporaryDirectory let domain = NSFileProviderDomain(identifier: .test, displayName: "Foo", pathRelativeToDocumentStorage: "/") - let itemStub = FileProviderItem(metadata: .init(name: "Foo", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: CloudPath("/foo"), isPlaceholderItem: false), domainIdentifier: .test, fullVersionChecker: FullVersionCheckerMock()) + let itemStub = FileProviderItem(metadata: .init(name: "Foo", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: CloudPath("/foo"), isPlaceholderItem: false), domainIdentifier: .test) override func setUpWithError() throws { notificatorMock = FileProviderNotificatorTypeMock() @@ -29,13 +30,13 @@ class FileImportingServiceSourceTests: XCTestCase { adapterProvidingMock = FileProviderAdapterProvidingMock() fullVersionCheckerMock = FullVersionCheckerMock() fullVersionCheckerMock.isFullVersion = true + DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) taskRegistratorMock = SessionTaskRegistratorMock() serviceSource = FileImportingServiceSource(domain: domain, notificator: notificatorMock, dbPath: dbPath, delegate: urlProviderMock, adapterManager: adapterProvidingMock, - fullVersionChecker: fullVersionCheckerMock, taskRegistrator: taskRegistratorMock) } diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift index a7882b36f..ca98e991a 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift @@ -9,6 +9,7 @@ import CryptomatorCloudAccessCore import XCTest @testable import CryptomatorFileProvider +@testable import Dependencies class FileProviderAdapterEnumerateItemTests: FileProviderAdapterTestCase { override func setUpWithError() throws { @@ -34,6 +35,9 @@ class FileProviderAdapterEnumerateItemTests: FileProviderAdapterTestCase { ItemMetadata(id: 3, name: "TestFolder", type: .file, size: nil, parentID: 4, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Foo/TestFolder"), isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: 1, tagData: nil) ] metadataManagerMock.workingSetMetadata = mockMetadata + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading let expectation = XCTestExpectation() adapter.enumerateItems(for: .workingSet, withPageToken: nil).then { itemList in XCTAssertEqual(mockMetadata.map { FileProviderItem(metadata: $0, domainIdentifier: .test) }, itemList.items) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift index 2c83892f7..e2f4fb8cd 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift @@ -12,6 +12,7 @@ import Promises import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { let itemID: Int64 = 2 @@ -26,6 +27,9 @@ class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { // MARK: LocalItemImport func testLocalItemImport() throws { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading let fileURL = tmpDirectory.appendingPathComponent("ItemToBeImported.txt", isDirectory: false) let fileContent = "TestContent" try fileContent.write(to: fileURL, atomically: true, encoding: .utf8) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift index 97568ed13..1931101c1 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift @@ -11,6 +11,7 @@ import Promises import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class FileProviderAdapterTestCase: CloudTaskExecutorTestCase { let fileCoordinator = NSFileCoordinator() @@ -27,6 +28,7 @@ class FileProviderAdapterTestCase: CloudTaskExecutorTestCase { fileProviderItemUpdateDelegateMock = FileProviderItemUpdateDelegateMock() fullVersionCheckerMock = FullVersionCheckerMock() fullVersionCheckerMock.isFullVersion = true + DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) taskRegistratorMock = SessionTaskRegistratorMock() adapter = FileProviderAdapter(domainIdentifier: .test, uploadTaskManager: uploadTaskManagerMock, @@ -41,7 +43,6 @@ class FileProviderAdapterTestCase: CloudTaskExecutorTestCase { coordinator: fileCoordinator, notificator: fileProviderItemUpdateDelegateMock, localURLProvider: localURLProviderMock, - fullVersionChecker: fullVersionCheckerMock, taskRegistrator: taskRegistratorMock) uploadTaskManagerMock.createNewTaskRecordForClosure = { return UploadTaskRecord(correspondingItem: $0.id!, lastFailedUploadDate: nil, uploadErrorCode: nil, uploadErrorDomain: nil) diff --git a/CryptomatorFileProviderTests/FileProviderAdapterManagerTests.swift b/CryptomatorFileProviderTests/FileProviderAdapterManagerTests.swift index be46a6f86..683b8e843 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapterManagerTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapterManagerTests.swift @@ -34,10 +34,6 @@ class FileProviderAdapterManagerTests: XCTestCase { case test } - override class func setUp() { - GlobalFullVersionChecker.default = FullVersionCheckerMock() - } - #warning("TODO: Replace unlockMonitor with mock") override func setUpWithError() throws { masterkeyCacheManagerMock = MasterkeyCacheManagerMock() diff --git a/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift b/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift index ecc2721b1..912b7bc19 100644 --- a/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift +++ b/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift @@ -13,6 +13,7 @@ import Promises import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class FileProviderEnumeratorTestCase: XCTestCase { var enumerationObserverMock: NSFileProviderEnumerationObserverMock! @@ -25,8 +26,8 @@ class FileProviderEnumeratorTestCase: XCTestCase { let dbPath = FileManager.default.temporaryDirectory let domain = NSFileProviderDomain(vaultUID: "VaultUID-12345", displayName: "Test Vault") let items: [FileProviderItem] = [ - .init(metadata: ItemMetadata(id: 2, name: "Test.txt", type: .file, size: 100, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test.txt"), isPlaceholderItem: false), domainIdentifier: .test, fullVersionChecker: FullVersionCheckerMock()), - .init(metadata: ItemMetadata(id: 3, name: "TestFolder", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestFolder"), isPlaceholderItem: false), domainIdentifier: .test, fullVersionChecker: FullVersionCheckerMock()) + .init(metadata: ItemMetadata(id: 2, name: "Test.txt", type: .file, size: 100, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test.txt"), isPlaceholderItem: false), domainIdentifier: .test), + .init(metadata: ItemMetadata(id: 3, name: "TestFolder", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestFolder"), isPlaceholderItem: false), domainIdentifier: .test) ] let deleteItemIdentifiers = [1, 2, 3].map { NSFileProviderItemIdentifier("\($0)") } @@ -50,6 +51,10 @@ class FileProviderEnumeratorTestCase: XCTestCase { } func assertChangeObserverUpdated(deletedItems: [NSFileProviderItemIdentifier], updatedItems: [FileProviderItem], currentSyncAnchor: NSFileProviderSyncAnchor) { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading + XCTAssertEqual([deletedItems], changeObserverMock.didDeleteItemsWithIdentifiersReceivedInvocations) let receivedUpdatedItems = changeObserverMock.didUpdateReceivedInvocations as? [[FileProviderItem]] XCTAssertEqual([updatedItems], receivedUpdatedItems) @@ -179,6 +184,10 @@ class FileProviderEnumeratorTests: FileProviderEnumeratorTestCase { } private func assertEnumerateItemObserverSucceeded(itemList: FileProviderItemList) { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading + XCTAssertEqual([itemList.nextPageToken], enumerationObserverMock.finishEnumeratingUpToReceivedInvocations) let receivedInvocations = enumerationObserverMock.didEnumerateReceivedInvocations as? [[FileProviderItem]] XCTAssertEqual([items], receivedInvocations) diff --git a/CryptomatorFileProviderTests/FileProviderItemTests.swift b/CryptomatorFileProviderTests/FileProviderItemTests.swift index 25ebeee9b..e8829ef6c 100644 --- a/CryptomatorFileProviderTests/FileProviderItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderItemTests.swift @@ -11,6 +11,7 @@ import MobileCoreServices import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class FileProviderItemTests: XCTestCase { func testRootItem() { @@ -107,54 +108,19 @@ class FileProviderItemTests: XCTestCase { // MARK: Capabilities - func testUploadingItemRestrictsCapabilityToRead() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = true + func testCapabilitiesArePassedThroughFromPermissionProvider() { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) let cloudPath = CloudPath("/test.txt") let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, fullVersionChecker: fullVersionCheckerMock) - XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, item.capabilities) - } - - func testUploadingFolderDoesNotRestrictCapabilities() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = true - - let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, fullVersionChecker: fullVersionCheckerMock) - XCTAssertEqual([.allowsAddingSubItems, .allowsContentEnumerating, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting], item.capabilities) - } - - func testCapabilitiesForRestrictedVersion() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = false - - let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, fullVersionChecker: fullVersionCheckerMock) - XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, item.capabilities) - } - - func testFailedUploadItemCapabilitiesForRestrictedVersion() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = false - - let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, fullVersionChecker: fullVersionCheckerMock) - XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, item.capabilities) - } - - func testFailedUploadFolderCapabilitiesForRestrictedVersion() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = false + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) - let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, fullVersionChecker: fullVersionCheckerMock) - XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, item.capabilities) + let capabilities: [NSFileProviderItemCapabilities] = [.allowsAddingSubItems, .allowsContentEnumerating, .allowsDeleting, .allowsReading, .allowsReparenting, .allowsWriting] + for capability in capabilities { + permissionProviderMock.getPermissionsForAtReturnValue = capability + XCTAssertEqual(capability, item.capabilities) + } } // MARK: Evict File From Cache Action diff --git a/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift b/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift index 650d54507..4e54026a2 100644 --- a/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift +++ b/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift @@ -9,6 +9,7 @@ import CryptomatorCloudAccessCore import XCTest @testable import CryptomatorFileProvider +@testable import Dependencies @available(iOS 14.0, *) class FileProviderNotificatorTests: XCTestCase { @@ -97,6 +98,11 @@ class FileProviderNotificatorTests: XCTestCase { }) let actualItems = notificator.popUpdateContainerItems() as? [FileProviderItem] + + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading + XCTAssertEqual([updatedItem], actualItems?.sorted()) XCTAssert(notificator.popUpdateWorkingSetItems().isEmpty) XCTAssert(notificator.getItemIdentifiersToDeleteFromWorkingSet().isEmpty) @@ -109,6 +115,9 @@ class FileProviderNotificatorTests: XCTestCase { } private func assertUpdateWorkingSetHasUpdatedItems() { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading let actualItems = notificator.popUpdateWorkingSetItems() as? [FileProviderItem] XCTAssertEqual(updatedItems.sorted(), actualItems?.sorted()) } diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/CloudTaskExecutorTestCase.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/CloudTaskExecutorTestCase.swift index cf9126186..157a855ee 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/CloudTaskExecutorTestCase.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/CloudTaskExecutorTestCase.swift @@ -24,10 +24,6 @@ class CloudTaskExecutorTestCase: XCTestCase { var deleteItemHelper: DeleteItemHelper! var tmpDirectory: URL! - override class func setUp() { - GlobalFullVersionChecker.default = FullVersionCheckerMock() - } - override func setUpWithError() throws { cloudProviderMock = CustomCloudProviderMock() metadataManagerMock = MetadataManagerMock() diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift index 05dc2be97..1c4ca9b14 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift @@ -10,6 +10,7 @@ import CryptomatorCloudAccessCore import Promises import XCTest @testable import CryptomatorFileProvider +@testable import Dependencies class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { override func setUpWithError() throws { @@ -201,6 +202,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { // MARK: Folder + // swiftlint:disable:next function_body_length func testFolderEnumeration() throws { let expectation = XCTestExpectation(description: "Folder Enumeration") @@ -222,6 +224,9 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let expectedSubFolderFileProviderItems = expectedItemMetadataInsideSubFolder.map { FileProviderItem(metadata: $0, domainIdentifier: .test) } let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading taskExecutor.execute(task: enumerationTask).then { fileProviderItemList -> FileProviderItem in XCTAssertEqual(5, fileProviderItemList.items.count) @@ -283,6 +288,10 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { FileProviderItem(metadata: ItemMetadata(id: 6, name: "File 4", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 4"), isPlaceholderItem: false), domainIdentifier: .test), FileProviderItem(metadata: ItemMetadata(id: 7, name: "NewFileFromCloud", type: .file, size: 24, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/NewFileFromCloud"), isPlaceholderItem: false), domainIdentifier: .test)] + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) taskExecutor.execute(task: enumerationTask).then { fileProviderItemList -> Promise in diff --git a/CryptomatorFileProviderTests/Mocks/CustomCloudProviderMockTests.swift b/CryptomatorFileProviderTests/Mocks/CustomCloudProviderMockTests.swift index 3cceb3b4b..38cb1f9c0 100644 --- a/CryptomatorFileProviderTests/Mocks/CustomCloudProviderMockTests.swift +++ b/CryptomatorFileProviderTests/Mocks/CustomCloudProviderMockTests.swift @@ -1,5 +1,5 @@ // -// CloudProviderMockTests.swift +// CustomCloudProviderMockTests.swift // CryptomatorFileProviderTests // // Created by Philipp Schmid on 01.07.20. diff --git a/CryptomatorFileProviderTests/Mocks/PermissionProviderMock.swift b/CryptomatorFileProviderTests/Mocks/PermissionProviderMock.swift new file mode 100644 index 000000000..7571ceee7 --- /dev/null +++ b/CryptomatorFileProviderTests/Mocks/PermissionProviderMock.swift @@ -0,0 +1,51 @@ +// +// PermissionProviderMock.swift +// CryptomatorFileProviderTests +// +// Created by Philipp Schmid on 19.09.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import CryptomatorFileProvider +import FileProvider +import Foundation + +final class PermissionProviderMock: PermissionProvider { + // MARK: - getPermissions + + var getPermissionsForAtCallsCount = 0 + var getPermissionsForAtCalled: Bool { + getPermissionsForAtCallsCount > 0 + } + + var getPermissionsForAtReceivedArguments: (item: ItemMetadata, domain: NSFileProviderDomainIdentifier)? + var getPermissionsForAtReceivedInvocations: [(item: ItemMetadata, domain: NSFileProviderDomainIdentifier)] = [] + var getPermissionsForAtReturnValue: NSFileProviderItemCapabilities! + var getPermissionsForAtClosure: ((ItemMetadata, NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities)? + + func getPermissions(for item: ItemMetadata, at domain: NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities { + getPermissionsForAtCallsCount += 1 + getPermissionsForAtReceivedArguments = (item: item, domain: domain) + getPermissionsForAtReceivedInvocations.append((item: item, domain: domain)) + return getPermissionsForAtClosure.map({ $0(item, domain) }) ?? getPermissionsForAtReturnValue + } + + // MARK: - getPermissionsForRootItem + + var getPermissionsForRootItemAtCallsCount = 0 + var getPermissionsForRootItemAtCalled: Bool { + getPermissionsForRootItemAtCallsCount > 0 + } + + var getPermissionsForRootItemAtReceivedDomain: NSFileProviderDomainIdentifier? + var getPermissionsForRootItemAtReceivedInvocations: [NSFileProviderDomainIdentifier?] = [] + var getPermissionsForRootItemAtReturnValue: NSFileProviderItemCapabilities! + var getPermissionsForRootItemAtClosure: ((NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities)? + + func getPermissionsForRootItem(at domain: NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities { + getPermissionsForRootItemAtCallsCount += 1 + getPermissionsForRootItemAtReceivedDomain = domain + getPermissionsForRootItemAtReceivedInvocations.append(domain) + return getPermissionsForRootItemAtClosure.map({ $0(domain) }) ?? getPermissionsForRootItemAtReturnValue + } +} diff --git a/CryptomatorFileProviderTests/PermissionProviderImplTests.swift b/CryptomatorFileProviderTests/PermissionProviderImplTests.swift new file mode 100644 index 000000000..67fdb5a8e --- /dev/null +++ b/CryptomatorFileProviderTests/PermissionProviderImplTests.swift @@ -0,0 +1,137 @@ +// +// PermissionProviderImplTests.swift +// CryptomatorFileProviderTests +// +// Created by Philipp Schmid on 19.09.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import XCTest +@testable import CryptomatorCommonCore +@testable import CryptomatorFileProvider +@testable import Dependencies + +final class PermissionProviderImplTests: XCTestCase { + private static let defaultFolderCapabilities: NSFileProviderItemCapabilities = [.allowsAddingSubItems, .allowsContentEnumerating, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] + private var fullVersionCheckerMock: FullVersionCheckerMock! + private var hubRepositoryMock: HubRepositoryMock! + private var permissionProvider: PermissionProviderImpl! + + override func setUpWithError() throws { + fullVersionCheckerMock = FullVersionCheckerMock() + hubRepositoryMock = HubRepositoryMock() + DependencyValues.mockDependency(\.hubRepository, with: hubRepositoryMock) + DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) + permissionProvider = PermissionProviderImpl() + } + + // MARK: Full Version + + func testUploadingItemRestrictsCapabilityToRead() { + fullVersionCheckerMock.isFullVersion = true + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) + } + + func testUploadingFolderDoesNotRestrictCapabilities() { + fullVersionCheckerMock.isFullVersion = true + + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(Self.defaultFolderCapabilities, actualCapabilities) + } + + func testCapabilitiesForRestrictedVersion() { + fullVersionCheckerMock.isFullVersion = false + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) + } + + func testFailedUploadItemCapabilitiesForRestrictedVersion() { + fullVersionCheckerMock.isFullVersion = false + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, actualCapabilities) + } + + func testFailedUploadFolderCapabilitiesForRestrictedVersion() { + fullVersionCheckerMock.isFullVersion = false + + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, actualCapabilities) + } + + func testFullVersionNoActiveHubScriptionReturnsFullPermissionsForFile() { + fullVersionCheckerMock.isFullVersion = true + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .inactive) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual([.allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting], actualCapabilities) + } + + // MARK: Cryptomator Hub + + func testUploadingItemRestrictsCapabilityToReadWithActiveHubSubscription() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) + } + + func testNoFullVersionNoActiveHubSubscriptionRestrictsToReadOnly() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .inactive) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) + } + + func testFolderCapabilitiesNoFullVersionActiveHubSubscription() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(Self.defaultFolderCapabilities, actualCapabilities) + } + + func testUploadingFolderDoesNotRestrictCapabilitiesForActiveHubSubsription() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) + + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(Self.defaultFolderCapabilities, actualCapabilities) + } + + func testNoFullVersionActiveHubScriptionReturnsFullPermissionsForFile() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual([.allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting], actualCapabilities) + } +} diff --git a/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift b/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift index 77c284290..eee1b96de 100644 --- a/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift +++ b/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift @@ -11,6 +11,7 @@ import Promises import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class CacheManagingServiceSourceTests: XCTestCase { var serviceSource: CacheManagingServiceSource! @@ -20,10 +21,6 @@ class CacheManagingServiceSourceTests: XCTestCase { let domains = [NSFileProviderDomain(identifier: NSFileProviderDomainIdentifier("1")), NSFileProviderDomain(identifier: NSFileProviderDomainIdentifier("2"))] - override class func setUp() { - GlobalFullVersionChecker.default = FullVersionCheckerMock() - } - override func setUpWithError() throws { cacheManagerFactoryMock = CachedFileManagerFactoryMock() domainProviderMock = NSFileProviderDomainProviderMock() @@ -61,6 +58,9 @@ class CacheManagingServiceSourceTests: XCTestCase { let expectation = XCTestExpectation() let cacheManagerMock = CachedFileManagerMock() cacheManagerFactoryMock.createCachedFileManagerForReturnValue = cacheManagerMock + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading let domainIdentifier = NSFileProviderDomainIdentifier("Test-Domain") let itemID: Int64 = 2 let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: domainIdentifier, itemID: itemID) diff --git a/CryptomatorFileProviderTests/WorkingSetObserverTests.swift b/CryptomatorFileProviderTests/WorkingSetObserverTests.swift index 034a0bca1..728b31357 100644 --- a/CryptomatorFileProviderTests/WorkingSetObserverTests.swift +++ b/CryptomatorFileProviderTests/WorkingSetObserverTests.swift @@ -10,6 +10,7 @@ import CryptomatorCloudAccessCore import GRDB import XCTest @testable import CryptomatorFileProvider +@testable import Dependencies class WorkingSetObserverTests: XCTestCase { var observer: WorkingSetObserver! @@ -31,6 +32,9 @@ class WorkingSetObserverTests: XCTestCase { XCTAssertEqual(1, notificatorMock.updateWorkingSetItemsCallsCount) let actualUpdatedItems = notificatorMock.updateWorkingSetItemsReceivedItems as? [FileProviderItem] + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading XCTAssertEqual(updatedItems.sorted(), actualUpdatedItems?.sorted()) XCTAssertEqual(1, notificatorMock.refreshWorkingSetCallsCount) } diff --git a/CryptomatorIntents/GetFolderIntentHandler.swift b/CryptomatorIntents/GetFolderIntentHandler.swift index 0b822f0b5..14653985f 100644 --- a/CryptomatorIntents/GetFolderIntentHandler.swift +++ b/CryptomatorIntents/GetFolderIntentHandler.swift @@ -9,12 +9,14 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import Foundation import Intents import Promises class GetFolderIntentHandler: NSObject, GetFolderIntentHandling { let vaultOptionsProvider: VaultOptionsProvider + @Dependency(\.fileProviderConnector) private var fileProviderConnector init(vaultOptionsProvider: VaultOptionsProvider) { self.vaultOptionsProvider = vaultOptionsProvider @@ -69,7 +71,7 @@ class GetFolderIntentHandler: NSObject, GetFolderIntentHandling { // MARK: Internal private func getIdentifierForFolder(at cloudPath: CloudPath, domainIdentifier: NSFileProviderDomainIdentifier) async throws -> String { - let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .fileImporting, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .fileImporting, domainIdentifier: domainIdentifier) return try await withCheckedThrowingContinuation({ continuation in getXPCPromise.then { xpc in xpc.proxy.getIdentifierForItem(at: cloudPath.path) @@ -77,8 +79,8 @@ class GetFolderIntentHandler: NSObject, GetFolderIntentHandling { continuation.resume(returning: $0 as String) }.catch { continuation.resume(throwing: $0) - }.always { - FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + }.always { [fileProviderConnector] in + fileProviderConnector.invalidateXPC(getXPCPromise) } }) } diff --git a/CryptomatorIntents/IntentHandler.swift b/CryptomatorIntents/IntentHandler.swift index 8f3ed8101..9f4202b02 100644 --- a/CryptomatorIntents/IntentHandler.swift +++ b/CryptomatorIntents/IntentHandler.swift @@ -34,13 +34,5 @@ class IntentHandler: INExtension { private static var oneTimeSetup: () -> Void = { // Set up logger LoggerSetup.oneTimeSetup() - if let dbURL = CryptomatorDatabase.sharedDBURL { - do { - let dbPool = try CryptomatorDatabase.openSharedDatabase(at: dbURL) - CryptomatorDatabase.shared = try CryptomatorDatabase(dbPool) - } catch { - DDLogError("Open shared database at \(dbURL) failed with error: \(error)") - } - } } } diff --git a/CryptomatorIntents/IsVaultUnlockedIntentHandler.swift b/CryptomatorIntents/IsVaultUnlockedIntentHandler.swift index 0e21a8c3a..dd5aa81f2 100644 --- a/CryptomatorIntents/IsVaultUnlockedIntentHandler.swift +++ b/CryptomatorIntents/IsVaultUnlockedIntentHandler.swift @@ -7,6 +7,7 @@ // import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import Intents @@ -14,6 +15,7 @@ import Promises class IsVaultUnlockedIntentHandler: NSObject, IsVaultUnlockedIntentHandling { let vaultOptionsProvider: VaultOptionsProvider + @Dependency(\.fileProviderConnector) private var fileProviderConnector init(vaultOptionsProvider: VaultOptionsProvider) { self.vaultOptionsProvider = vaultOptionsProvider @@ -46,7 +48,7 @@ class IsVaultUnlockedIntentHandler: NSObject, IsVaultUnlockedIntentHandling { // MARK: Internal private func getIsUnlockedVault(domainIdentifier: NSFileProviderDomainIdentifier) async throws -> Bool { - let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) return try await withCheckedThrowingContinuation({ continuation in getXPCPromise.then { xpc in xpc.proxy.getIsUnlockedVault(domainIdentifier: domainIdentifier) @@ -54,8 +56,8 @@ class IsVaultUnlockedIntentHandler: NSObject, IsVaultUnlockedIntentHandling { continuation.resume(returning: $0) }.catch { continuation.resume(throwing: $0) - }.always { - FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + }.always { [fileProviderConnector] in + fileProviderConnector.invalidateXPC(getXPCPromise) } }) } diff --git a/CryptomatorIntents/LockVaultIntentHandler.swift b/CryptomatorIntents/LockVaultIntentHandler.swift index aea7e3ead..a3065ce08 100644 --- a/CryptomatorIntents/LockVaultIntentHandler.swift +++ b/CryptomatorIntents/LockVaultIntentHandler.swift @@ -8,12 +8,14 @@ import CocoaLumberjackSwift import CryptomatorCommonCore +import Dependencies import Foundation import Intents import Promises class LockVaultIntentHandler: NSObject, LockVaultIntentHandling { let vaultOptionsProvider: VaultOptionsProvider + @Dependency(\.fileProviderConnector) private var fileProviderConnector init(vaultOptionsProvider: VaultOptionsProvider) { self.vaultOptionsProvider = vaultOptionsProvider @@ -45,7 +47,7 @@ class LockVaultIntentHandler: NSObject, LockVaultIntentHandling { // MARK: Internal private func lockVault(with domainIdentifier: NSFileProviderDomainIdentifier) async throws { - let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) return try await withCheckedThrowingContinuation({ continuation in getXPCPromise.then { xpc in xpc.proxy.gracefulLockVault(domainIdentifier: domainIdentifier) @@ -53,8 +55,8 @@ class LockVaultIntentHandler: NSObject, LockVaultIntentHandling { continuation.resume(returning: ()) }.catch { continuation.resume(throwing: $0) - }.always { - FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + }.always { [fileProviderConnector] in + fileProviderConnector.invalidateXPC(getXPCPromise) } }) } diff --git a/CryptomatorIntents/SaveFileIntentHandler.swift b/CryptomatorIntents/SaveFileIntentHandler.swift index 400f1c064..00dafa1d9 100644 --- a/CryptomatorIntents/SaveFileIntentHandler.swift +++ b/CryptomatorIntents/SaveFileIntentHandler.swift @@ -9,12 +9,15 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import Intents import Promises class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { + @Dependency(\.fileProviderConnector) private var fileProviderConnector + func handle(intent: SaveFileIntent) async -> SaveFileIntentResponse { guard let vaultFolder = intent.folder, let vaultIdentifier = vaultFolder.vaultIdentifier, let folderIdentifier = vaultFolder.identifier else { return SaveFileIntentResponse(failureReason: LocalizedString.getValue("intents.saveFile.invalidFolder")) @@ -85,7 +88,7 @@ class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { } private func importFile(at localURL: URL, toParentItemIdentifier parentItemIdentifier: String, domainIdentifier: NSFileProviderDomainIdentifier) async throws { - let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .fileImporting, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .fileImporting, domainIdentifier: domainIdentifier) try await withCheckedThrowingContinuation({ continuation in getXPCPromise.then { xpc in xpc.proxy.importFile(at: localURL, toParentItemIdentifier: parentItemIdentifier) @@ -93,8 +96,8 @@ class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { continuation.resume() }.catch { continuation.resume(throwing: $0) - }.always { - FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + }.always { [fileProviderConnector] in + fileProviderConnector.invalidateXPC(getXPCPromise) } }) } diff --git a/CryptomatorIntents/ar.lproj/Intents.strings b/CryptomatorIntents/ar.lproj/Intents.strings index 844de668e..aa2d90733 100644 --- a/CryptomatorIntents/ar.lproj/Intents.strings +++ b/CryptomatorIntents/ar.lproj/Intents.strings @@ -1 +1 @@ -"common.vault" = "مخزن"; +"common.vault" = "الخزينة"; diff --git a/CryptomatorIntents/ba.lproj/Intents.strings b/CryptomatorIntents/ba.lproj/Intents.strings new file mode 100644 index 000000000..3b50fb0cb --- /dev/null +++ b/CryptomatorIntents/ba.lproj/Intents.strings @@ -0,0 +1,33 @@ +"common.failureReason" = "Уңышһыҙлыҡ сәбәбе"; +"common.false" = "юҡ"; +"common.folder" = "Каталог"; +"common.true" = "эйе"; +"common.vault" = "Һаҡлағыс"; + +"getFolderIntent.description" = "Күрһәтелгән һаҡлағыста бирелгән юл өсөн каталог объектын кире ҡайтара."; +"getFolderIntent.path" = "Юл"; +"getFolderIntent.text" = "${vault} эсендәге ${path} юллы каталогты ал"; +"getFolderIntent.title" = "Каталогты ал"; + +"isUnlockedIntent.description" = "Күрһәтелгән һаҡлағыстың асыҡмы икәнен күрһәтә."; +"isUnlockedIntent.title" = "Бик асыу"; + +"isVaultLockedIntent.title" = "Һаҡлағыс биге асыҡ"; +"isVaultUnlockedIntent.text" = "${vault} биге асыҡмы?"; + +"lockVaultIntent.description" = "Күрһәтелгән һаҡлағысты бикләй."; +"lockVaultIntent.text" = "${vault} биклә"; +"lockVaultIntent.title" = "Һаҡлағысты бикләү"; + +"openVaultIntent.description" = "Файлдар ҡушымтаһында күрһәтелгән һаҡлағысты аса."; +"openVaultIntent.text" = "Файлдар ҡушымтаһында ${vault} ас"; +"openVaultIntent.title" = "Һаҡлағыс асыу"; + +"saveFileIntent.description" = "Файлды һаҡлағысҡа һаҡлай."; +"saveFileIntent.file" = "Файл"; +"saveFileIntent.parameter.ignoreExisting" = "Бер иш исемле файлды иғтибарһыҙ ҡалдырырға"; +"saveFileIntent.text" = "${file} файлы ${folder} каталогына һаҡлай"; +"saveFileIntent.title" = "Файлды һаҡлау"; + +"vaultFolder.displayName" = "Һаҡлағыс каталогы"; +"vaultFolder.vaultIdentifier" = "Һаҡлағыс идентификаторы"; diff --git a/CryptomatorIntents/be.lproj/Intents.strings b/CryptomatorIntents/be.lproj/Intents.strings index 02ccf3728..ae03bff71 100644 --- a/CryptomatorIntents/be.lproj/Intents.strings +++ b/CryptomatorIntents/be.lproj/Intents.strings @@ -3,3 +3,11 @@ "isVaultLockedIntent.title" = "Ці разамкнёна скарбніца"; "isVaultUnlockedIntent.text" = "Ці ${vault} разамкнёны?"; +"openVaultIntent.title" = "Адчыніць скарбніцу"; + +"saveFileIntent.description" = "Захаваць файл у скарбніцы."; +"saveFileIntent.file" = "Файл"; +"saveFileIntent.title" = "Захаваць файл"; + +"vaultFolder.displayName" = "Тэчка скарбніцы"; +"vaultFolder.vaultIdentifier" = "Ідэнтыфікатар скарбніцы"; diff --git a/CryptomatorIntents/bg.lproj/Intents.strings b/CryptomatorIntents/bg.lproj/Intents.strings new file mode 100644 index 000000000..3c6af9eda --- /dev/null +++ b/CryptomatorIntents/bg.lproj/Intents.strings @@ -0,0 +1 @@ +"common.vault" = "Хранилище"; diff --git a/CryptomatorIntents/cs.lproj/Intents.strings b/CryptomatorIntents/cs.lproj/Intents.strings index a78d3bc66..d0d2470bd 100644 --- a/CryptomatorIntents/cs.lproj/Intents.strings +++ b/CryptomatorIntents/cs.lproj/Intents.strings @@ -9,6 +9,12 @@ "getFolderIntent.text" = "Získat složku, nacházející se v ${path} v ${vault}"; "getFolderIntent.title" = "Získat složku"; +"isUnlockedIntent.description" = "Vrátí, zda je daný trezor odemčený."; +"isUnlockedIntent.title" = "Je Odemčen"; + +"isVaultLockedIntent.title" = "Je trezor odemčen"; +"isVaultUnlockedIntent.text" = "Je ${vault} odemčen?"; + "lockVaultIntent.description" = "Uzamkne daný trezor."; "lockVaultIntent.text" = "Zamknout ${vault}"; "lockVaultIntent.title" = "Uzamknout trezor"; diff --git a/CryptomatorIntents/fa.lproj/Intents.strings b/CryptomatorIntents/fa.lproj/Intents.strings index 369e3fabf..60dbd875d 100644 --- a/CryptomatorIntents/fa.lproj/Intents.strings +++ b/CryptomatorIntents/fa.lproj/Intents.strings @@ -1 +1,21 @@ +"common.false" = "غلط"; +"common.folder" = "پوشه"; +"common.true" = "درست"; "common.vault" = "گاوصندوق"; +"getFolderIntent.path" = "مسیر"; +"getFolderIntent.title" = "دریافت پوشه"; +"isUnlockedIntent.title" = "باز شده است"; + +"isVaultLockedIntent.title" = "گاوصندوق باز شده است"; +"isVaultUnlockedIntent.text" = "آیا ${vault} باز شده است؟"; +"lockVaultIntent.text" = "قفل کردن ${vault}"; +"lockVaultIntent.title" = "قفل کردن گاوصندوق"; +"openVaultIntent.title" = "باز کردن گاوصندوق"; + +"saveFileIntent.description" = "یک فایل در گاوصندوق ذخیره میشود."; +"saveFileIntent.file" = "فایل"; +"saveFileIntent.text" = "ذخیره ${file} در ${folder}"; +"saveFileIntent.title" = "دخیرهٔ پرونده‌"; + +"vaultFolder.displayName" = "پوشه گاوصندوق"; +"vaultFolder.vaultIdentifier" = "مشخص کننده گاوصندوق"; diff --git a/CryptomatorIntents/fi.lproj/Intents.strings b/CryptomatorIntents/fi.lproj/Intents.strings new file mode 100644 index 000000000..c369e1822 --- /dev/null +++ b/CryptomatorIntents/fi.lproj/Intents.strings @@ -0,0 +1 @@ +"common.vault" = "Vault"; diff --git a/CryptomatorIntents/fil.lproj/Intents.strings b/CryptomatorIntents/fil.lproj/Intents.strings index c369e1822..7bad93386 100644 --- a/CryptomatorIntents/fil.lproj/Intents.strings +++ b/CryptomatorIntents/fil.lproj/Intents.strings @@ -1 +1,33 @@ +"common.failureReason" = "Dahilan ng Pagkabigo"; +"common.false" = "mali"; +"common.folder" = "Folder"; +"common.true" = "totoo"; "common.vault" = "Vault"; + +"getFolderIntent.description" = "Nagbabalik ng folder object para sa ibinigay na path sa ibinigay na vault."; +"getFolderIntent.path" = "Daan"; +"getFolderIntent.text" = "Kunin ang folder na matatagpuan sa ${path} sa ${vault}"; +"getFolderIntent.title" = "Kumuha ng Folder"; + +"isUnlockedIntent.description" = "Ibinabalik kung naka-unlock ang ibinigay na vault."; +"isUnlockedIntent.title" = "Naka-unlock"; + +"isVaultLockedIntent.title" = "Naka-unlock ang vault"; +"isVaultUnlockedIntent.text" = "Naka-unlock ba ang ${vault}?"; + +"lockVaultIntent.description" = "Nila-lock ang ibinigay na vault."; +"lockVaultIntent.text" = "I-lock ang ${vault}"; +"lockVaultIntent.title" = "Lock Vault"; + +"openVaultIntent.description" = "Binubuksan ang ibinigay na vault sa Files app."; +"openVaultIntent.text" = "Buksan ang ${vault} sa Files app"; +"openVaultIntent.title" = "Buksan ang Vault"; + +"saveFileIntent.description" = "Nagse-save ng file sa isang vault."; +"saveFileIntent.file" = "file"; +"saveFileIntent.parameter.ignoreExisting" = "Huwag pansinin ang umiiral na file na may parehong pangalan"; +"saveFileIntent.text" = "I-save ang ${file} sa ${folder}"; +"saveFileIntent.title" = "I-save ang File"; + +"vaultFolder.displayName" = "Vault Folder"; +"vaultFolder.vaultIdentifier" = "Vault Identifier"; diff --git a/CryptomatorIntents/hu.lproj/Intents.strings b/CryptomatorIntents/hu.lproj/Intents.strings index 5398e7ff9..92eddd9b1 100644 --- a/CryptomatorIntents/hu.lproj/Intents.strings +++ b/CryptomatorIntents/hu.lproj/Intents.strings @@ -1 +1,21 @@ +"common.false" = "hamis"; +"common.folder" = "Mappa"; +"common.true" = "igaz"; "common.vault" = "Széf"; + +"getFolderIntent.description" = "Visszaad egy mappa objektumot a megadott útvonalhoz a megadott széfben."; +"getFolderIntent.path" = "Útvonal"; +"getFolderIntent.text" = "Visszaadja a mappát ezen a helyen: ${path}, ebben a széfben: ${vault}"; + +"isUnlockedIntent.description" = "Visszaadja, hogy az adott széf fel van-e oldva."; +"lockVaultIntent.text" = "${vault} zárolása"; +"lockVaultIntent.title" = "Széf zárolása"; + +"openVaultIntent.description" = "Megnyitja az adott széfet a Fájlok appban."; +"openVaultIntent.text" = "${vault} megnyitása a Fájlok appban"; +"openVaultIntent.title" = "Széf megnyitása"; + +"saveFileIntent.description" = "Elment egy fájlt egy széfbe."; + +"vaultFolder.displayName" = "Széf mappa"; +"vaultFolder.vaultIdentifier" = "Széf azonosító"; diff --git a/CryptomatorIntents/ja.lproj/Intents.strings b/CryptomatorIntents/ja.lproj/Intents.strings index f1e94a15e..79aa1b66b 100644 --- a/CryptomatorIntents/ja.lproj/Intents.strings +++ b/CryptomatorIntents/ja.lproj/Intents.strings @@ -10,7 +10,7 @@ "getFolderIntent.title" = "フォルダーを戻す"; "isUnlockedIntent.description" = "金庫が解錠かどうか戻します。"; -"isUnlockedIntent.title" = "解錠です"; +"isUnlockedIntent.title" = "解錠済"; "isVaultLockedIntent.title" = "金庫が解錠ですか?"; "isVaultUnlockedIntent.text" = "${vault} が解錠してありますか?"; diff --git a/CryptomatorIntents/ko.lproj/Intents.strings b/CryptomatorIntents/ko.lproj/Intents.strings index c369e1822..f302a3d9a 100644 --- a/CryptomatorIntents/ko.lproj/Intents.strings +++ b/CryptomatorIntents/ko.lproj/Intents.strings @@ -1 +1,16 @@ +"common.failureReason" = "실패 사유"; +"common.folder" = "폴더"; "common.vault" = "Vault"; +"getFolderIntent.path" = "경로"; +"getFolderIntent.title" = "폴더 열기"; + +"lockVaultIntent.description" = "해당 vault를 잠급니다."; +"lockVaultIntent.title" = "Vault 잠그기"; +"openVaultIntent.title" = "Vault 열기"; +"saveFileIntent.file" = "파일"; +"saveFileIntent.parameter.ignoreExisting" = "같은 이름을 가진 기존 파일 무시하기"; +"saveFileIntent.text" = "${file}을 ${folder}에 저장"; +"saveFileIntent.title" = "파일 저장하기"; + +"vaultFolder.displayName" = "Vault 폴더"; +"vaultFolder.vaultIdentifier" = "Vault 식별자"; diff --git a/CryptomatorIntents/mk.lproj/Intents.strings b/CryptomatorIntents/mk.lproj/Intents.strings index e69de29bb..74c0da3a1 100644 --- a/CryptomatorIntents/mk.lproj/Intents.strings +++ b/CryptomatorIntents/mk.lproj/Intents.strings @@ -0,0 +1 @@ +"common.vault" = "Сеф"; diff --git a/CryptomatorIntents/pa.lproj/Intents.strings b/CryptomatorIntents/pa.lproj/Intents.strings index ac591e060..b17370b42 100644 --- a/CryptomatorIntents/pa.lproj/Intents.strings +++ b/CryptomatorIntents/pa.lproj/Intents.strings @@ -1 +1,31 @@ +"common.failureReason" = "ਫੇਲ੍ਹ ਹੋਣ ਦਾ ਕਾਰਨ"; +"common.false" = "ਅਸਫ਼ਲ"; +"common.folder" = "ਫੋਲਡਰ"; +"common.true" = "ਸੱਚ"; "common.vault" = "ਵਾਲਟ"; + +"getFolderIntent.description" = "ਦਿੱਤੇ ਵਾਲਟ ਵਿੱਚ ਦਿੱਤੇ ਹੋਏ ਮਾਰਗ ਲਈ ਫੋਲਡਰ ਆਬਜੈਕਟ ਵਾਪਸ ਦਿੰਦਾ ਹੈ।"; +"getFolderIntent.path" = "ਮਾਰਗ"; +"getFolderIntent.text" = "${vault} ਵਿੱਚ ${path} ਉੱਤੇ ਮੌਜੂਦ ਫੋਲਡਰ ਲਵੋ"; +"getFolderIntent.title" = "ਫੋਲਡਰ ਲਵੋ"; +"isUnlockedIntent.title" = "ਅਣ-ਲਾਕ ਹੈ"; + +"isVaultLockedIntent.title" = "ਵਾਲਟ ਅਣ-ਲਾਕ ਹੈ"; +"isVaultUnlockedIntent.text" = "${vault} ਅਣ-ਲਾਕ ਕਰਨਾ ਹੈ?"; + +"lockVaultIntent.description" = "ਦਿੱਤੇ ਵਾਲਟ ਨੂੰ ਲਾਕ ਕਰਦਾ ਹੈ।"; +"lockVaultIntent.text" = "${vault} ਨੂੰ ਲਾਕ ਕਰੋ"; +"lockVaultIntent.title" = "ਵਾਲਟ ਲਾਕ ਕਰੋ"; + +"openVaultIntent.description" = "ਫਾਇਲ ਐਪ ਵਿੱਚ ਦਿੱਤਾ ਵਾਲਟ ਖੋਲ੍ਹੋ।"; +"openVaultIntent.text" = "ਫਾਇਲਾਂ ਐਪ ਵਿੱਚ ${vault} ਨੂੰ ਖੋਲ੍ਹੋ"; +"openVaultIntent.title" = "ਵਾਲਟ ਖੋਲ੍ਹੋ"; + +"saveFileIntent.description" = "ਫਾਇਲ ਨੂੰ ਵਾਲਟ ਵਿੱਚ ਖੋਲ੍ਹੋ।"; +"saveFileIntent.file" = "ਫਾਇਲ"; +"saveFileIntent.parameter.ignoreExisting" = "ਇੱਕੋ ਨਾਂ ਦੀ ਮੌਜੂਦਾ ਫਾਇਲ ਨੂੰ ਅਣਡਿੱਠਾ ਕਰੋ"; +"saveFileIntent.text" = "${folder} ਵਿੱਚ ${file} ਸੰਭਾਲੋ"; +"saveFileIntent.title" = "ਫਾਇਲ ਸੰਭਾਲੋ"; + +"vaultFolder.displayName" = "ਵਾਲਟ ਫੋਲਡਰ"; +"vaultFolder.vaultIdentifier" = "ਵਾਲਟ ਪਛਾਣਕਰਤਾ"; diff --git a/CryptomatorIntents/ta.lproj/Intents.strings b/CryptomatorIntents/ta.lproj/Intents.strings index 79f04a5ed..159544fa2 100644 --- a/CryptomatorIntents/ta.lproj/Intents.strings +++ b/CryptomatorIntents/ta.lproj/Intents.strings @@ -1 +1,2 @@ +"common.false" = "தவறு"; "common.vault" = "பெட்டகம்"; diff --git a/CryptomatorIntents/zh-Hant.lproj/Intents.strings b/CryptomatorIntents/zh-Hant.lproj/Intents.strings index e7ec1c531..bdba1f45f 100644 --- a/CryptomatorIntents/zh-Hant.lproj/Intents.strings +++ b/CryptomatorIntents/zh-Hant.lproj/Intents.strings @@ -2,7 +2,7 @@ "common.false" = "否"; "common.folder" = "資料夾"; "common.true" = "是"; -"common.vault" = "保险库"; +"common.vault" = "加密檔案庫"; "getFolderIntent.description" = "返回特定加密檔案庫內指定路徑的資料夾。"; "getFolderIntent.path" = "路徑"; diff --git a/CryptomatorTests/AccountListViewModelTests.swift b/CryptomatorTests/AccountListViewModelTests.swift index ec447d042..c3e3ddf7c 100644 --- a/CryptomatorTests/AccountListViewModelTests.swift +++ b/CryptomatorTests/AccountListViewModelTests.swift @@ -13,27 +13,9 @@ import XCTest @testable import CryptomatorCommonCore class AccountListViewModelTests: XCTestCase { - var tmpDir: URL! - var dbPool: DatabasePool! - var cryptomatorDB: CryptomatorDatabase! - override func setUpWithError() throws { - tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true, attributes: nil) - let dbURL = tmpDir.appendingPathComponent("db.sqlite") - dbPool = try CryptomatorDatabase.openSharedDatabase(at: dbURL) - cryptomatorDB = try CryptomatorDatabase(dbPool) - _ = try DatabaseManager(dbPool: dbPool) - } - - override func tearDownWithError() throws { - dbPool = nil - cryptomatorDB = nil - try FileManager.default.removeItem(at: tmpDir) - } - func testMoveRow() throws { - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let accountManager = CloudProviderAccountDBManager(dbPool: dbPool) + let dbManagerMock = try DatabaseManagerMock() + let accountManager = CloudProviderAccountDBManager() let cloudAuthenticatorMock = CloudAuthenticatorMock(accountManager: accountManager) let accountListViewModel = AccountListViewModelMock(with: .dropbox, dbManager: dbManagerMock, cloudAuthenticator: cloudAuthenticatorMock) try accountListViewModel.refreshItems() @@ -57,8 +39,8 @@ class AccountListViewModelTests: XCTestCase { } func testRemoveRow() throws { - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let accountManager = CloudProviderAccountDBManager(dbPool: dbPool) + let dbManagerMock = DatabaseManagerMock() + let accountManager = CloudProviderAccountDBManager() let cloudAuthenticatorMock = CloudAuthenticatorMock(accountManager: accountManager) let accountListViewModel = AccountListViewModelMock(with: .dropbox, dbManager: dbManagerMock, cloudAuthenticator: cloudAuthenticatorMock) try accountListViewModel.refreshItems() @@ -78,8 +60,8 @@ class AccountListViewModelTests: XCTestCase { } func testWebDAVAccountCellContent() throws { - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let accountManager = CloudProviderAccountDBManager(dbPool: dbPool) + let dbManagerMock = DatabaseManagerMock() + let accountManager = CloudProviderAccountDBManager() let cloudAuthenticatorMock = CloudAuthenticatorMock(accountManager: accountManager) let accountListViewModel = AccountListViewModel(with: .dropbox, dbManager: dbManagerMock, cloudAuthenticator: cloudAuthenticatorMock) let baseURL = URL(string: "https://www.example.com")! @@ -90,8 +72,8 @@ class AccountListViewModelTests: XCTestCase { } func testWebDAVAccountCellContentWithPathInDetailLabel() throws { - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let accountManager = CloudProviderAccountDBManager(dbPool: dbPool) + let dbManagerMock = DatabaseManagerMock() + let accountManager = CloudProviderAccountDBManager() let cloudAuthenticatorMock = CloudAuthenticatorMock(accountManager: accountManager) let accountListViewModel = AccountListViewModel(with: .dropbox, dbManager: dbManagerMock, cloudAuthenticator: cloudAuthenticatorMock) let baseURL = URL(string: "https://www.example.com/path")! @@ -102,8 +84,8 @@ class AccountListViewModelTests: XCTestCase { } func testWebDAVAccountCellContentWithUnknownHost() throws { - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let accountManager = CloudProviderAccountDBManager(dbPool: dbPool) + let dbManagerMock = DatabaseManagerMock() + let accountManager = CloudProviderAccountDBManager() let cloudAuthenticatorMock = CloudAuthenticatorMock(accountManager: accountManager) let accountListViewModel = AccountListViewModel(with: .dropbox, dbManager: dbManagerMock, cloudAuthenticator: cloudAuthenticatorMock) let baseURL = URL(string: "www")! diff --git a/CryptomatorTests/ChangePasswordViewModelTests.swift b/CryptomatorTests/ChangePasswordViewModelTests.swift index bf12f6add..0210d8267 100644 --- a/CryptomatorTests/ChangePasswordViewModelTests.swift +++ b/CryptomatorTests/ChangePasswordViewModelTests.swift @@ -14,6 +14,7 @@ import Promises import XCTest @testable import Cryptomator @testable import CryptomatorCommonCore +@testable import Dependencies class ChangePasswordViewModelTests: XCTestCase { private var vaultManagerMock: VaultManagerMock! @@ -27,7 +28,8 @@ class ChangePasswordViewModelTests: XCTestCase { setupMocks() vaultAccount = VaultAccount(vaultUID: UUID().uuidString, delegateAccountUID: UUID().uuidString, vaultPath: CloudPath("/Foo/Bar"), vaultName: "Bar") let domain = NSFileProviderDomain(vaultUID: vaultAccount.vaultUID, displayName: vaultAccount.vaultName) - viewModel = ChangePasswordViewModel(vaultAccount: vaultAccount, domain: domain, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) + DependencyValues.mockDependency(\.fileProviderConnector, with: fileProviderConnectorMock) + viewModel = ChangePasswordViewModel(vaultAccount: vaultAccount, domain: domain, vaultManager: vaultManagerMock) } private func setupMocks() { @@ -70,7 +72,7 @@ class ChangePasswordViewModelTests: XCTestCase { try await viewModel.changePassword() - wait(for: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) + await fulfillment(of: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) XCTAssertEqual(1, vaultManagerMock.changePassphraseOldPassphraseNewPassphraseForVaultUIDCallsCount) XCTAssertEqual(oldPassword, vaultManagerMock.changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedArguments?.oldPassphrase) @@ -125,7 +127,7 @@ class ChangePasswordViewModelTests: XCTestCase { XCTAssertEqual(1, vaultLockingMock.lockedVaults.count) XCTAssertTrue(vaultLockingMock.lockedVaults.contains(NSFileProviderDomainIdentifier(vaultAccount.vaultUID))) - wait(for: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) + await fulfillment(of: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) } func testChangePasswordFailForEmptyOldPassword() async throws { diff --git a/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift b/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift index 9b92fa039..72ca2b8ab 100644 --- a/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift +++ b/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift @@ -186,6 +186,14 @@ private class PasswordVaultManagerMock: VaultManager { func createVaultProvider(withUID vaultUID: String, masterkey: Masterkey) throws -> CloudProvider { throw MockError.notMocked } + + func addExistingHubVault(_ vault: ExistingHubVault) -> Promise { + return Promise(MockError.notMocked) + } + + func manualUnlockVault(withUID vaultUID: String, rawKey: [UInt8]) throws -> CloudProvider { + throw MockError.notMocked + } } private struct CreatedVault { diff --git a/CryptomatorTests/DatabaseManagerTests.swift b/CryptomatorTests/DatabaseManagerTests.swift index 15671a1b4..5faa9892d 100644 --- a/CryptomatorTests/DatabaseManagerTests.swift +++ b/CryptomatorTests/DatabaseManagerTests.swift @@ -11,39 +11,38 @@ import GRDB import XCTest @testable import Cryptomator @testable import CryptomatorCommonCore +@testable import Dependencies class DatabaseManagerTests: XCTestCase { var tmpDir: URL! - var dbPool: DatabasePool! var dbManager: DatabaseManager! - var cryptomatorDB: CryptomatorDatabase! + override func setUpWithError() throws { tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true, attributes: nil) let dbURL = tmpDir.appendingPathComponent("db.sqlite") - dbPool = try CryptomatorDatabase.openSharedDatabase(at: dbURL) - cryptomatorDB = try CryptomatorDatabase(dbPool) - dbManager = try DatabaseManager(dbPool: dbPool) + + DependencyValues.mockDependency(\.databaseLocation, with: dbURL) + DependencyValues.mockDependency(\.database, with: CryptomatorDatabase.live) + dbManager = DatabaseManager() } override func tearDownWithError() throws { - dbPool = nil - cryptomatorDB = nil - dbManager = nil try FileManager.default.removeItem(at: tmpDir) } // MARK: VaultListPosition func testCreatePositionTrigger() throws { - let cloudAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) - let vaultAccountManager = VaultAccountDBManager(dbPool: dbPool) + @Dependency(\.database) var database + let cloudAccountManager = CloudProviderAccountDBManager() + let vaultAccountManager = VaultAccountDBManager() let cloudProviderAccount = CloudProviderAccount(accountUID: "1", cloudProviderType: .dropbox) try cloudAccountManager.saveNewAccount(cloudProviderAccount) let vaultAccount = VaultAccount(vaultUID: "Vault1", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault1"), vaultName: "Vault1") try vaultAccountManager.saveNewAccount(vaultAccount) - let firstVaultListPosition = try dbPool.read { db in + let firstVaultListPosition = try database.read { db in try VaultListPosition.filter(Column("vaultUID") == "Vault1").fetchOne(db) } XCTAssertNotNil(firstVaultListPosition) @@ -53,7 +52,7 @@ class DatabaseManagerTests: XCTestCase { let secondVaultAccount = VaultAccount(vaultUID: "Vault2", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault2"), vaultName: "Vault2") try vaultAccountManager.saveNewAccount(secondVaultAccount) - let secondVaultListPosition = try dbPool.read { db in + let secondVaultListPosition = try database.read { db in try VaultListPosition.filter(Column("vaultUID") == "Vault2").fetchOne(db) } XCTAssertNotNil(secondVaultListPosition) @@ -62,8 +61,10 @@ class DatabaseManagerTests: XCTestCase { } func testDeleteVaultAccountUpdatesPositions() throws { - let cloudAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) - let vaultAccountManager = VaultAccountDBManager(dbPool: dbPool) + @Dependency(\.database) var database + + let cloudAccountManager = CloudProviderAccountDBManager() + let vaultAccountManager = VaultAccountDBManager() let cloudProviderAccount = CloudProviderAccount(accountUID: "1", cloudProviderType: .dropbox) try cloudAccountManager.saveNewAccount(cloudProviderAccount) @@ -74,22 +75,22 @@ class DatabaseManagerTests: XCTestCase { let thirdVaultAccount = VaultAccount(vaultUID: "Vault3", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault3"), vaultName: "Vault3") try vaultAccountManager.saveNewAccount(thirdVaultAccount) - _ = try dbPool.write { db in + _ = try database.write { db in try vaultAccount.delete(db) } - let vaultListPositionEntryForVault1 = try dbPool.read { db in + let vaultListPositionEntryForVault1 = try database.read { db in try VaultListPosition.filter(Column("vaultUID") == "Vault1").fetchOne(db) } XCTAssertNil(vaultListPositionEntryForVault1) - let firstVaultListPosition = try dbPool.read { db in + let firstVaultListPosition = try database.read { db in try VaultListPosition.filter(Column("vaultUID") == "Vault2").fetchOne(db) } XCTAssertNotNil(firstVaultListPosition) XCTAssertEqual(0, firstVaultListPosition?.position) - let secondVaultListPosition = try dbPool.read { db in + let secondVaultListPosition = try database.read { db in try VaultListPosition.filter(Column("vaultUID") == "Vault3").fetchOne(db) } XCTAssertNotNil(secondVaultListPosition) @@ -97,8 +98,8 @@ class DatabaseManagerTests: XCTestCase { } func testUpdateVaultListPositions() throws { - let cloudAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) - let vaultAccountManager = VaultAccountDBManager(dbPool: dbPool) + let cloudAccountManager = CloudProviderAccountDBManager() + let vaultAccountManager = VaultAccountDBManager() let cloudProviderAccount = CloudProviderAccount(accountUID: "1", cloudProviderType: .dropbox) try cloudAccountManager.saveNewAccount(cloudProviderAccount) @@ -130,7 +131,8 @@ class DatabaseManagerTests: XCTestCase { // MARK: AccountListPosition func testCreateAccountListPositionTrigger() throws { - let cloudAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) + @Dependency(\.database) var database + let cloudAccountManager = CloudProviderAccountDBManager() let firstWebdavCloudProviderAccount = CloudProviderAccount(accountUID: "firstWebdavCloudProviderAccount", cloudProviderType: .webDAV(type: .custom)) try cloudAccountManager.saveNewAccount(firstWebdavCloudProviderAccount) @@ -141,21 +143,21 @@ class DatabaseManagerTests: XCTestCase { let firstDropboxCloudProviderAccount = CloudProviderAccount(accountUID: "firstDropboxCloudProviderAccount", cloudProviderType: .dropbox) try cloudAccountManager.saveNewAccount(firstDropboxCloudProviderAccount) - let firstWebDAVAccountListPosition = try dbPool.read { db in + let firstWebDAVAccountListPosition = try database.read { db in try AccountListPosition.filter(Column("accountUID") == "firstWebdavCloudProviderAccount" && Column("cloudProviderType") == CloudProviderType.webDAV(type: .custom)).fetchOne(db) } XCTAssertNotNil(firstWebDAVAccountListPosition) XCTAssertEqual(0, firstWebDAVAccountListPosition?.position) XCTAssertEqual(1, firstWebDAVAccountListPosition?.id) - let secondWebDAVAccountListPosition = try dbPool.read { db in + let secondWebDAVAccountListPosition = try database.read { db in try AccountListPosition.filter(Column("accountUID") == "secondWebdavCloudProviderAccount" && Column("cloudProviderType") == CloudProviderType.webDAV(type: .custom)).fetchOne(db) } XCTAssertNotNil(secondWebDAVAccountListPosition) XCTAssertEqual(1, secondWebDAVAccountListPosition?.position) XCTAssertEqual(2, secondWebDAVAccountListPosition?.id) - let firstDropboxAccountListPosition = try dbPool.read { db in + let firstDropboxAccountListPosition = try database.read { db in try AccountListPosition.filter(Column("accountUID") == "firstDropboxCloudProviderAccount" && Column("cloudProviderType") == CloudProviderType.dropbox).fetchOne(db) } XCTAssertNotNil(firstDropboxAccountListPosition) @@ -164,7 +166,8 @@ class DatabaseManagerTests: XCTestCase { } func testDeleteCloudProviderAccountUpdatesPositions() throws { - let cloudAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) + @Dependency(\.database) var database + let cloudAccountManager = CloudProviderAccountDBManager() let firstWebdavCloudProviderAccount = CloudProviderAccount(accountUID: "firstWebdavCloudProviderAccount", cloudProviderType: .webDAV(type: .custom)) try cloudAccountManager.saveNewAccount(firstWebdavCloudProviderAccount) @@ -178,28 +181,28 @@ class DatabaseManagerTests: XCTestCase { let firstDropboxCloudProviderAccount = CloudProviderAccount(accountUID: "firstDropboxCloudProviderAccount", cloudProviderType: .dropbox) try cloudAccountManager.saveNewAccount(firstDropboxCloudProviderAccount) - _ = try dbPool.write { db in + _ = try database.write { db in try firstWebdavCloudProviderAccount.delete(db) } - let accountListPositionEntryForFirstWebDAVAccount = try dbPool.read { db in + let accountListPositionEntryForFirstWebDAVAccount = try database.read { db in try AccountListPosition.filter(Column("accountUID") == "firstWebdavCloudProviderAccount").fetchOne(db) } XCTAssertNil(accountListPositionEntryForFirstWebDAVAccount) - let firstAccountListPositionForWebDAV = try dbPool.read { db in + let firstAccountListPositionForWebDAV = try database.read { db in try AccountListPosition.filter(Column("accountUID") == "secondWebdavCloudProviderAccount").fetchOne(db) } XCTAssertNotNil(firstAccountListPositionForWebDAV) XCTAssertEqual(0, firstAccountListPositionForWebDAV?.position) - let secondAccountListPositionForWebDAV = try dbPool.read { db in + let secondAccountListPositionForWebDAV = try database.read { db in try AccountListPosition.filter(Column("accountUID") == "thirdWebdavCloudProviderAccount").fetchOne(db) } XCTAssertNotNil(secondAccountListPositionForWebDAV) XCTAssertEqual(1, secondAccountListPositionForWebDAV?.position) - let firstAccountListPositionForDropbox = try dbPool.read { db in + let firstAccountListPositionForDropbox = try database.read { db in try AccountListPosition.filter(Column("accountUID") == "firstDropboxCloudProviderAccount").fetchOne(db) } XCTAssertNotNil(firstAccountListPositionForDropbox) @@ -207,7 +210,7 @@ class DatabaseManagerTests: XCTestCase { } func testUpdateAccountListPositions() throws { - let cloudAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) + let cloudAccountManager = CloudProviderAccountDBManager() let firstWebdavCloudProviderAccount = CloudProviderAccount(accountUID: "firstWebdavCloudProviderAccount", cloudProviderType: .webDAV(type: .custom)) try cloudAccountManager.saveNewAccount(firstWebdavCloudProviderAccount) @@ -239,7 +242,7 @@ class DatabaseManagerTests: XCTestCase { } func testGetAllAccountsIsFiltered() throws { - let cloudAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) + let cloudAccountManager = CloudProviderAccountDBManager() let firstWebdavCloudProviderAccount = CloudProviderAccount(accountUID: "firstWebdavCloudProviderAccount", cloudProviderType: .webDAV(type: .custom)) try cloudAccountManager.saveNewAccount(firstWebdavCloudProviderAccount) diff --git a/CryptomatorTests/Mocks/IAPManagerMock.swift b/CryptomatorTests/Mocks/IAPManagerMock.swift index b073d22c4..f3a70b405 100644 --- a/CryptomatorTests/Mocks/IAPManagerMock.swift +++ b/CryptomatorTests/Mocks/IAPManagerMock.swift @@ -1,5 +1,5 @@ // -// IAPManager.swift +// IAPManagerMock.swift // CryptomatorTests // // Created by Philipp Schmid on 26.11.21. diff --git a/CryptomatorTests/MoveVaultViewModelTests.swift b/CryptomatorTests/MoveVaultViewModelTests.swift index d68a9da4c..a0d1a4b09 100644 --- a/CryptomatorTests/MoveVaultViewModelTests.swift +++ b/CryptomatorTests/MoveVaultViewModelTests.swift @@ -13,6 +13,7 @@ import Promises import XCTest @testable import Cryptomator @testable import CryptomatorCommonCore +@testable import Dependencies class MoveVaultViewModelTests: XCTestCase { private var vaultManagerMock: VaultManagerMock! @@ -36,6 +37,8 @@ class MoveVaultViewModelTests: XCTestCase { maintenanceHelperMock = MaintenanceModeHelperMock() vaultLockingMock = VaultLockingMock() + DependencyValues.mockDependency(\.fileProviderConnector, with: fileProviderConnectorMock) + fileProviderConnectorMock.getXPCServiceNameDomainClosure = { serviceName, _ in switch serviceName { case .maintenanceModeHelper: @@ -71,7 +74,7 @@ class MoveVaultViewModelTests: XCTestCase { XCTAssertEqual(1, vaultLockingMock.lockedVaults.count) XCTAssertTrue(vaultLockingMock.lockedVaults.contains(NSFileProviderDomainIdentifier(vaultAccount.vaultUID))) - wait(for: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) + await fulfillment(of: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) } func testRejectVaultsInTheLocalFileSystem() async throws { @@ -173,7 +176,6 @@ class MoveVaultViewModelTests: XCTestCase { currentFolderChoosingCloudPath: currentFolderChoosingCloudPath, vaultInfo: vaultInfo, domain: domain, - vaultManager: vaultManagerMock, - fileProviderConnector: fileProviderConnectorMock) + vaultManager: vaultManagerMock) } } diff --git a/CryptomatorTests/Purchase/StoreObserverTests.swift b/CryptomatorTests/Purchase/StoreObserverTests.swift index 3e129d245..a8a6adfc0 100644 --- a/CryptomatorTests/Purchase/StoreObserverTests.swift +++ b/CryptomatorTests/Purchase/StoreObserverTests.swift @@ -42,32 +42,25 @@ class StoreObserverTests: XCTestCase { // MARK: Buy Product - func testBuyFreeTrial() throws { - let expectation = XCTestExpectation() - storeManager.fetchProducts(with: [.thirtyDayTrial]).then { response -> Promise in - XCTAssertEqual(1, response.products.count) - return self.storeObserver.buy(response.products[0]) - }.then { purchaseTransaction in - try self.assertTrialStarted(purchaseTransaction: purchaseTransaction) - }.catch { error in - XCTFail("Promise failed with error: \(error)") - }.always { - expectation.fulfill() - } - wait(for: [expectation], timeout: 1.0) + func testBuyFreeTrial() async throws { + let response = try await storeManager.fetchProducts(with: [.thirtyDayTrial]).getValue() + XCTAssertEqual(1, response.products.count) + + let purchaseTransaction = try await storeObserver.buy(response.products[0]).getValue() + try assertTrialStarted(purchaseTransaction: purchaseTransaction) } - func testBuyFullVersion() throws { - assertFullVersionUnlockedWhenBuying(product: .fullVersion) - assertFullVersionUnlockedWhenBuying(product: .paidUpgrade) - assertFullVersionUnlockedWhenBuying(product: .freeUpgrade) + func testBuyFullVersion() async throws { + try await assertFullVersionUnlockedWhenBuying(product: .fullVersion) + try await assertFullVersionUnlockedWhenBuying(product: .paidUpgrade) + try await assertFullVersionUnlockedWhenBuying(product: .freeUpgrade) } // MARK: Deferred Transactions (Ask to buy) // Only test the approved case as there is no transaction state changes if the transaction gets declined // see https://developer.apple.com/forums/thread/685183 - func testAskToBuy() throws { + func testAskToBuy() async throws { session.askToBuyEnabled = true XCTAssert(session.allTransactions().isEmpty) @@ -84,42 +77,42 @@ class StoreObserverTests: XCTestCase { } storeObserver.fallbackDelegate = fallbackDelegateMock - assertBuyFailsWithDeferredTransactionError() + try await assertBuyFailsWithDeferredTransactionError() try approveAskToBuyTransaction() - wait(for: [fallbackCalledExpectation], timeout: 1.0) + await fulfillment(of: [fallbackCalledExpectation]) XCTAssertEqual(1, fallbackDelegateMock.purchaseDidSucceedTransactionCallsCount) } - func testRestoreRunningSubscription() { + func testRestoreRunningSubscription() async throws { let cryptomatorSettingsMock = CryptomatorSettingsMock() cryptomatorSettingsMock.hasRunningSubscription = true - assertRestored(with: .restoredFullVersion, cryptomatorSettings: cryptomatorSettingsMock) + try await assertRestored(with: .restoredFullVersion, cryptomatorSettings: cryptomatorSettingsMock) } - func testRestoreLifetimePremium() { + func testRestoreLifetimePremium() async throws { let cryptomatorSettingsMock = CryptomatorSettingsMock() cryptomatorSettingsMock.fullVersionUnlocked = true - assertRestored(with: .restoredFullVersion, cryptomatorSettings: cryptomatorSettingsMock) + try await assertRestored(with: .restoredFullVersion, cryptomatorSettings: cryptomatorSettingsMock) } - func testRestoreTrial() { + func testRestoreTrial() async throws { let cryptomatorSettingsMock = CryptomatorSettingsMock() let trialExpirationDate = Date.distantFuture cryptomatorSettingsMock.trialExpirationDate = trialExpirationDate - assertRestored(with: .restoredFreeTrial(expiresOn: trialExpirationDate), cryptomatorSettings: cryptomatorSettingsMock) + try await assertRestored(with: .restoredFreeTrial(expiresOn: trialExpirationDate), cryptomatorSettings: cryptomatorSettingsMock) } - func testRestoreExpiredTrial() { + func testRestoreExpiredTrial() async throws { let cryptomatorSettingsMock = CryptomatorSettingsMock() let trialExpirationDate = Date.distantPast cryptomatorSettingsMock.trialExpirationDate = trialExpirationDate - assertRestored(with: .noRestorablePurchases, cryptomatorSettings: cryptomatorSettingsMock) + try await assertRestored(with: .noRestorablePurchases, cryptomatorSettings: cryptomatorSettingsMock) } - func testRestoreNothing() { + func testRestoreNothing() async throws { let cryptomatorSettingsMock = CryptomatorSettingsMock() - assertRestored(with: .noRestorablePurchases, cryptomatorSettings: cryptomatorSettingsMock) + try await assertRestored(with: .noRestorablePurchases, cryptomatorSettings: cryptomatorSettingsMock) } // MARK: - Internal @@ -134,20 +127,14 @@ class StoreObserverTests: XCTestCase { try session.approveAskToBuyTransaction(identifier: deferredTransaction.identifier) } - private func assertFullVersionUnlockedWhenBuying(product: ProductIdentifier) { - let expectation = XCTestExpectation() - storeManager.fetchProducts(with: [product]).then { response -> Promise in - XCTAssertEqual(1, response.products.count) - return self.storeObserver.buy(response.products[0]) - }.then { purchaseTransaction in - XCTAssertEqual(.fullVersion, purchaseTransaction) - XCTAssert(self.cryptomatorSettingsMock.fullVersionUnlocked) - }.catch { error in - XCTFail("Promise failed with error: \(error)") - }.always { - expectation.fulfill() - } - wait(for: [expectation], timeout: 1.0) + private func assertFullVersionUnlockedWhenBuying(product: ProductIdentifier, file: StaticString = #filePath, line: UInt = #line) async throws { + let response = try await storeManager.fetchProducts(with: [product]).getValue() + XCTAssertEqual(1, response.products.count) + + let purchaseTransaction = try await storeObserver.buy(response.products[0]).getValue() + + XCTAssertEqual(.fullVersion, purchaseTransaction) + XCTAssert(cryptomatorSettingsMock.fullVersionUnlocked) } private func assertTrialStarted(purchaseTransaction: PurchaseTransaction) throws { @@ -156,45 +143,37 @@ class StoreObserverTests: XCTestCase { XCTFail("Wrong purchaseTransaction: \(purchaseTransaction)") return } - XCTAssertEqual(expectedDate.timeIntervalSinceReferenceDate, expiresOn.timeIntervalSinceReferenceDate, accuracy: 2.0) + + // decrease the accuracy to 2 minutes to increase stability of the unit tests in the CI. + XCTAssertEqual(expectedDate.timeIntervalSinceReferenceDate, expiresOn.timeIntervalSinceReferenceDate, accuracy: 120.0) let actualDate = try XCTUnwrap(cryptomatorSettingsMock.trialExpirationDate, "trialExpirationDate was not set") - XCTAssertEqual(expectedDate.timeIntervalSinceReferenceDate, actualDate.timeIntervalSinceReferenceDate, accuracy: 2.0) - } - - private func assertBuyFailsWithDeferredTransactionError() { - let askToBuyExpectation = XCTestExpectation() - let fetchProductPromise = storeManager.fetchProducts(with: [.thirtyDayTrial]) - fetchProductPromise.then { response -> Promise in - XCTAssertEqual(1, response.products.count) - return self.storeObserver.buy(response.products[0]) - }.then { _ in - XCTFail("Promise fulfilled") - }.catch { error in + XCTAssertEqual(expectedDate.timeIntervalSinceReferenceDate, actualDate.timeIntervalSinceReferenceDate, accuracy: 120.0) + } + + private func assertBuyFailsWithDeferredTransactionError(file: StaticString = #filePath, line: UInt = #line) async throws { + let response = try await storeManager.fetchProducts(with: [.thirtyDayTrial]).getValue() + + XCTAssertEqual(1, response.products.count) + + do { + _ = try await storeObserver.buy(response.products[0]).getValue() + XCTFail("Buy did not fail", file: file, line: line) + } catch { XCTAssertEqual(.deferredTransaction, error as? StoreObserverError) - }.always { - askToBuyExpectation.fulfill() } - wait(for: [askToBuyExpectation], timeout: 1.0) } - private func assertRestored(with expectedResult: RestoreTransactionsResult, cryptomatorSettings: CryptomatorSettings) { - let expectation = XCTestExpectation() + private func assertRestored(with expectedResult: RestoreTransactionsResult, cryptomatorSettings: CryptomatorSettings, file: StaticString = #filePath, line: UInt = #line) async throws { let premiumManagerMock = PremiumManagerTypeMock() let storeObserver = StoreObserver(cryptomatorSettings: cryptomatorSettings, premiumManager: premiumManagerMock) SKPaymentQueue.default().add(storeObserver) SKPaymentQueue.default().remove(self.storeObserver) - storeObserver.restore().then { result in - XCTAssertEqual(expectedResult, result) - XCTAssert(premiumManagerMock.refreshStatusCalled) - }.catch { error in - XCTFail("Promise failed with error: \(error)") - }.always { - expectation.fulfill() - } - wait(for: [expectation], timeout: 1.0) + let result = try await storeObserver.restore().getValue() + XCTAssertEqual(expectedResult, result) + XCTAssert(premiumManagerMock.refreshStatusCalled) } } diff --git a/CryptomatorTests/RenameVaultViewModelTests.swift b/CryptomatorTests/RenameVaultViewModelTests.swift index 59390496d..331c51270 100644 --- a/CryptomatorTests/RenameVaultViewModelTests.swift +++ b/CryptomatorTests/RenameVaultViewModelTests.swift @@ -13,6 +13,7 @@ import Promises import XCTest @testable import Cryptomator @testable import CryptomatorCommonCore +@testable import Dependencies class RenameVaultViewModelTests: SetVaultNameViewModelTests { private var vaultManagerMock: VaultManagerMock! @@ -32,6 +33,8 @@ class RenameVaultViewModelTests: SetVaultNameViewModelTests { maintenanceHelperMock = MaintenanceModeHelperMock() vaultLockingMock = VaultLockingMock() + DependencyValues.mockDependency(\.fileProviderConnector, with: fileProviderConnectorMock) + fileProviderConnectorMock.getXPCServiceNameDomainClosure = { serviceName, _ in switch serviceName { case .maintenanceModeHelper: @@ -101,7 +104,7 @@ class RenameVaultViewModelTests: SetVaultNameViewModelTests { XCTAssertEqual(1, vaultLockingMock.lockedVaults.count) XCTAssertTrue(vaultLockingMock.lockedVaults.contains(NSFileProviderDomainIdentifier(vaultAccount.vaultUID))) - wait(for: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) + await fulfillment(of: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) } func testRenameVaultWithOldNameAsSubstring() async throws { @@ -191,7 +194,7 @@ class RenameVaultViewModelTests: SetVaultNameViewModelTests { XCTAssertEqual(1, vaultLockingMock.lockedVaults.count) XCTAssertTrue(vaultLockingMock.lockedVaults.contains(NSFileProviderDomainIdentifier(vaultAccount.vaultUID))) XCTAssertFalse(vaultManagerMock.moveVaultAccountToCalled) - wait(for: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) + await fulfillment(of: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) } private func createViewModel(vaultAccount: VaultAccount, cloudProviderType: CloudProviderType, viewControllerTitle: String? = nil) -> RenameVaultViewModel { @@ -199,7 +202,7 @@ class RenameVaultViewModelTests: SetVaultNameViewModelTests { let vaultListPosition = VaultListPosition(id: 1, position: 1, vaultUID: vaultAccount.vaultUID) let vaultInfo = VaultInfo(vaultAccount: vaultAccount, cloudProviderAccount: cloudProviderAccount, vaultListPosition: vaultListPosition) let domain = NSFileProviderDomain(vaultUID: vaultInfo.vaultUID, displayName: vaultInfo.vaultName) - return RenameVaultViewModel(provider: CloudProviderMock(), vaultInfo: vaultInfo, domain: domain, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) + return RenameVaultViewModel(provider: CloudProviderMock(), vaultInfo: vaultInfo, domain: domain, vaultManager: vaultManagerMock) } private func checkMaintenanceModeEnabledThenDisabled() { diff --git a/CryptomatorTests/S3AuthenticationViewModelTests.swift b/CryptomatorTests/S3AuthenticationViewModelTests.swift index 8043f3a25..8e529122a 100644 --- a/CryptomatorTests/S3AuthenticationViewModelTests.swift +++ b/CryptomatorTests/S3AuthenticationViewModelTests.swift @@ -113,7 +113,7 @@ class S3AuthenticationViewModelTests: XCTestCase { let recorder = viewModel.$loginState.recordNext(2) prepareViewModelWithDefaultValues() - viewModel.endpoint = "example invalid endpoint" + viewModel.endpoint = "https://example invalid endpoint" credentialVerifierMock.verifyCredentialReturnValue = Promise(()) viewModel.saveS3Credential() diff --git a/CryptomatorTests/SettingsViewModelTests.swift b/CryptomatorTests/SettingsViewModelTests.swift index 848651685..f2ed8ff85 100644 --- a/CryptomatorTests/SettingsViewModelTests.swift +++ b/CryptomatorTests/SettingsViewModelTests.swift @@ -11,6 +11,7 @@ import XCTest @testable import Cryptomator @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class SettingsViewModelTests: XCTestCase { private var cryptomatorSettingsMock: CryptomatorSettingsMock! @@ -25,7 +26,8 @@ class SettingsViewModelTests: XCTestCase { } cryptomatorSettingsMock = CryptomatorSettingsMock() fileProviderConnectorMock = FileProviderConnectorMock() - settingsViewModel = SettingsViewModel(cryptomatorSettings: cryptomatorSettingsMock, fileProviderConnector: fileProviderConnectorMock) + DependencyValues.mockDependency(\.fileProviderConnector, with: fileProviderConnectorMock) + settingsViewModel = SettingsViewModel(cryptomatorSettings: cryptomatorSettingsMock) } // - MARK: Cache Section diff --git a/CryptomatorTests/VaultKeepUnlockedViewModelTests.swift b/CryptomatorTests/VaultKeepUnlockedViewModelTests.swift index d3284f401..7ddb6d789 100644 --- a/CryptomatorTests/VaultKeepUnlockedViewModelTests.swift +++ b/CryptomatorTests/VaultKeepUnlockedViewModelTests.swift @@ -10,6 +10,7 @@ import CryptomatorCloudAccessCore import XCTest @testable import Cryptomator @testable import CryptomatorCommonCore +@testable import Dependencies class VaultKeepUnlockedViewModelTests: XCTestCase { var vaultKeepUnlockedSettingsMock: VaultKeepUnlockedSettingsMock! @@ -26,6 +27,7 @@ class VaultKeepUnlockedViewModelTests: XCTestCase { masterkeyCacheManagerMock = MasterkeyCacheManagerMock() fileProviderConnectorMock = FileProviderConnectorMock() vaultLockingMock = VaultLockingMock() + DependencyValues.mockDependency(\.fileProviderConnector, with: fileProviderConnectorMock) } func testDefaultConfiguration() throws { @@ -203,8 +205,7 @@ class VaultKeepUnlockedViewModelTests: XCTestCase { return VaultKeepUnlockedViewModel(currentKeepUnlockedDuration: currentKeepUnlockedDuration, vaultInfo: vaultInfo, vaultKeepUnlockedSettings: vaultKeepUnlockedSettingsMock, - masterkeyCacheManager: masterkeyCacheManagerMock, - fileProviderConnector: fileProviderConnectorMock) + masterkeyCacheManager: masterkeyCacheManagerMock) } private func assertSectionsAreCorrect(selectedKeepUnlockedDuration: KeepUnlockedDuration, viewModel: VaultKeepUnlockedViewModel) { diff --git a/CryptomatorTests/VaultListViewModelTests.swift b/CryptomatorTests/VaultListViewModelTests.swift index eb317c003..0decfa5c1 100644 --- a/CryptomatorTests/VaultListViewModelTests.swift +++ b/CryptomatorTests/VaultListViewModelTests.swift @@ -13,41 +13,28 @@ import Promises import XCTest @testable import Cryptomator @testable import CryptomatorCommonCore +@testable import Dependencies class VaultListViewModelTests: XCTestCase { - var tmpDir: URL! - var dbPool: DatabasePool! - var cryptomatorDB: CryptomatorDatabase! private var vaultManagerMock: VaultDBManagerMock! private var vaultAccountManagerMock: VaultAccountManagerMock! private var passwordManagerMock: VaultPasswordManagerMock! private var vaultCacheMock: VaultCacheMock! private var fileProviderConnectorMock: FileProviderConnectorMock! - override func setUpWithError() throws { - tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true, attributes: nil) - let dbURL = tmpDir.appendingPathComponent("db.sqlite") - dbPool = try CryptomatorDatabase.openSharedDatabase(at: dbURL) - cryptomatorDB = try CryptomatorDatabase(dbPool) - let cloudProviderManager = CloudProviderDBManager(accountManager: CloudProviderAccountDBManager(dbPool: dbPool)) + override func setUpWithError() throws { + let cloudProviderManager = CloudProviderDBManager(accountManager: CloudProviderAccountDBManager()) vaultAccountManagerMock = VaultAccountManagerMock() passwordManagerMock = VaultPasswordManagerMock() vaultCacheMock = VaultCacheMock() vaultManagerMock = VaultDBManagerMock(providerManager: cloudProviderManager, vaultAccountManager: vaultAccountManagerMock, vaultCache: vaultCacheMock, passwordManager: passwordManagerMock, masterkeyCacheManager: MasterkeyCacheManagerMock(), masterkeyCacheHelper: MasterkeyCacheHelperMock()) fileProviderConnectorMock = FileProviderConnectorMock() - _ = try DatabaseManager(dbPool: dbPool) - } - - override func tearDownWithError() throws { - dbPool = nil - cryptomatorDB = nil - try FileManager.default.removeItem(at: tmpDir) + DependencyValues.mockDependency(\.fileProviderConnector, with: fileProviderConnectorMock) } func testRefreshVaultsIsSorted() throws { - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) + let dbManagerMock = DatabaseManagerMock() + let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock) XCTAssert(vaultListViewModel.getVaults().isEmpty) try vaultListViewModel.refreshItems() XCTAssertEqual(2, vaultListViewModel.getVaults().count) @@ -59,8 +46,8 @@ class VaultListViewModelTests: XCTestCase { } func testMoveRow() throws { - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) + let dbManagerMock = DatabaseManagerMock() + let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock) try vaultListViewModel.refreshItems() XCTAssertEqual(0, vaultListViewModel.getVaults()[0].listPosition) @@ -82,8 +69,8 @@ class VaultListViewModelTests: XCTestCase { let cachedVault = CachedVault(vaultUID: "vault2", masterkeyFileData: "".data(using: .utf8)!, vaultConfigToken: nil, lastUpToDateCheck: Date(), masterkeyFileLastModifiedDate: nil, vaultConfigLastModifiedDate: nil) try vaultCacheMock.cache(cachedVault) - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) + let dbManagerMock = DatabaseManagerMock() + let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock) try vaultListViewModel.refreshItems() XCTAssertEqual(0, vaultListViewModel.getVaults()[0].listPosition) @@ -105,8 +92,8 @@ class VaultListViewModelTests: XCTestCase { func testLockVault() throws { let expectation = XCTestExpectation() - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) + let dbManagerMock = DatabaseManagerMock() + let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock) let vaultInfo = VaultInfo(vaultAccount: VaultAccount(vaultUID: "vault1", delegateAccountUID: "1", vaultPath: CloudPath("/vault1"), vaultName: "vault1"), cloudProviderAccount: CloudProviderAccount(accountUID: "1", cloudProviderType: .dropbox), vaultListPosition: VaultListPosition(position: 1, vaultUID: "vault1")) @@ -131,8 +118,8 @@ class VaultListViewModelTests: XCTestCase { func testRefreshVaultLockedStates() throws { let expectation = XCTestExpectation() - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) + let dbManagerMock = DatabaseManagerMock() + let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock) try vaultListViewModel.refreshItems() XCTAssertTrue(vaultListViewModel.getVaults().allSatisfy({ !$0.vaultIsUnlocked.value })) diff --git a/FileProviderExtension/FileProviderExtension.swift b/FileProviderExtension/FileProviderExtension.swift index 51f0e2ce4..2458a4f48 100644 --- a/FileProviderExtension/FileProviderExtension.swift +++ b/FileProviderExtension/FileProviderExtension.swift @@ -10,6 +10,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore import CryptomatorFileProvider +import Dependencies import FileProvider import MSAL @@ -25,27 +26,19 @@ class FileProviderExtension: NSFileProviderExtension { LoggerSetup.oneTimeSetup() FileProviderExtension.setupIAP() if !FileProviderExtension.sharedDatabaseInitialized { - if let dbURL = CryptomatorDatabase.sharedDBURL { - do { - let dbPool = try CryptomatorDatabase.openSharedDatabase(at: dbURL) - CryptomatorDatabase.shared = try CryptomatorDatabase(dbPool) - FileProviderExtension.sharedDatabaseInitialized = true - DropboxSetup.constants = DropboxSetup(appKey: CloudAccessSecrets.dropboxAppKey, sharedContainerIdentifier: CryptomatorConstants.appGroupName, keychainService: CryptomatorConstants.mainAppBundleId, forceForegroundSession: false) - GoogleDriveSetup.constants = GoogleDriveSetup(clientId: CloudAccessSecrets.googleDriveClientId, redirectURL: CloudAccessSecrets.googleDriveRedirectURL!, sharedContainerIdentifier: CryptomatorConstants.appGroupName) - OneDriveSetup.sharedContainerIdentifier = CryptomatorConstants.appGroupName - let oneDriveConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.oneDriveClientId, redirectUri: CloudAccessSecrets.oneDriveRedirectURI, authority: nil) - oneDriveConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId - OneDriveSetup.clientApplication = try MSALPublicClientApplication(configuration: oneDriveConfiguration) - } catch { - // MARK: Handle error - - FileProviderExtension.databaseError = error - DDLogError("Failed to initialize FPExt sharedDB: \(error)") - } - } else { + do { + FileProviderExtension.sharedDatabaseInitialized = true + DropboxSetup.constants = DropboxSetup(appKey: CloudAccessSecrets.dropboxAppKey, sharedContainerIdentifier: CryptomatorConstants.appGroupName, keychainService: CryptomatorConstants.mainAppBundleId, forceForegroundSession: false) + GoogleDriveSetup.constants = GoogleDriveSetup(clientId: CloudAccessSecrets.googleDriveClientId, redirectURL: CloudAccessSecrets.googleDriveRedirectURL!, sharedContainerIdentifier: CryptomatorConstants.appGroupName) + OneDriveSetup.sharedContainerIdentifier = CryptomatorConstants.appGroupName + let oneDriveConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.oneDriveClientId, redirectUri: CloudAccessSecrets.oneDriveRedirectURI, authority: nil) + oneDriveConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId + OneDriveSetup.clientApplication = try MSALPublicClientApplication(configuration: oneDriveConfiguration) + } catch { // MARK: Handle error - DDLogError("FPExt - dbURL is nil") + FileProviderExtension.databaseError = error + DDLogError("Failed to initialize FPExt sharedDB: \(error)") } } @@ -76,7 +69,7 @@ class FileProviderExtension: NSFileProviderExtension { // resolve the given identifier to a record in the model DDLogDebug("FPExt: item(for: \(identifier)) called") if identifier == .rootContainer || identifier.rawValue == "File Provider Storage" || identifier.rawValue == domain?.identifier.rawValue { - return RootFileProviderItem() + return RootFileProviderItem(domain: domain) } let adapter = try getAdapterWithWrappedError() return try adapter.item(for: identifier) @@ -316,15 +309,28 @@ class FileProviderExtension: NSFileProviderExtension { static var setupIAP: () -> Void = { #if ALWAYS_PREMIUM DDLogDebug("Always activated premium") - GlobalFullVersionChecker.default = AlwaysActivatedPremium.default + CryptomatorUserDefaults.shared.fullVersionUnlocked = true #else DDLogDebug("Freemium version") - GlobalFullVersionChecker.default = UserDefaultsFullVersionChecker.default #endif return {} }() } +/** + Define the liveValue in the main target since compilation flags do not work on Swift Package Manager level. + Be aware that it is needed to set the default value once per app launch (+ also when launching the FileProviderExtension). + */ +extension FullVersionCheckerKey: DependencyKey { + public static var liveValue: FullVersionChecker { + #if ALWAYS_PREMIUM + return AlwaysActivatedPremium.default + #else + return UserDefaultsFullVersionChecker.default + #endif + } +} + enum FileProviderDecoratorSetupError: Error { case fileProviderManagerIsNil case domainIsNil diff --git a/FileProviderExtensionUI/FileProviderCoordinator.swift b/FileProviderExtensionUI/FileProviderCoordinator.swift index 41b645a0b..40bc0ff09 100644 --- a/FileProviderExtensionUI/FileProviderCoordinator.swift +++ b/FileProviderExtensionUI/FileProviderCoordinator.swift @@ -7,13 +7,16 @@ // import CocoaLumberjackSwift +import CryptomatorCloudAccessCore +import CryptomatorCommon import CryptomatorCommonCore import CryptomatorFileProvider import FileProviderUI import LocalAuthentication import UIKit -class FileProviderCoordinator { +class FileProviderCoordinator: Coordinator { + lazy var childCoordinators = [Coordinator]() lazy var navigationController: UINavigationController = { let appearance = UINavigationBarAppearance() appearance.configureWithOpaqueBackground() @@ -39,6 +42,8 @@ class FileProviderCoordinator { extensionContext.cancelRequest(withError: NSError(domain: FPUIErrorDomain, code: Int(FPUIExtensionErrorCode.userCancelled.rawValue), userInfo: nil)) } + func start() {} + func startWith(error: Error) { let error = error as NSError let userInfo = error.userInfo @@ -91,7 +96,7 @@ class FileProviderCoordinator { if unlockError == .defaultLock, viewModel.canQuickUnlock { performQuickUnlock(viewModel: viewModel) } else { - showManualPasswordScreen(viewModel: viewModel) + showManualLogin(for: domain, unlockError: unlockError) } } @@ -113,6 +118,57 @@ class FileProviderCoordinator { } } + func showManualLogin(for domain: NSFileProviderDomain, unlockError: UnlockError) { + let vaultUID = domain.identifier.rawValue + let vaultCache = VaultDBCache() + let vaultAccount: VaultAccount + let provider: CloudProvider + do { + vaultAccount = try VaultAccountDBManager.shared.getAccount(with: vaultUID) + provider = try LocalizedCloudProviderDecorator(delegate: CloudProviderDBManager.shared.getProvider(with: vaultAccount.delegateAccountUID)) + } catch { + handleError(error) + return + } + vaultCache.refreshVaultCache(for: vaultAccount, with: provider).recover { error -> Void in + switch error { + case CloudProviderError.noInternetConnection, LocalizedCloudProviderError.itemNotFound: + break + default: + throw error + } + }.then { + let cachedVault = try vaultCache.getCachedVault(withVaultUID: vaultUID) + if let vaultConfigToken = cachedVault.vaultConfigToken { + let unverifiedVaultConfig = try UnverifiedVaultConfig(token: vaultConfigToken) + switch VaultConfigHelper.getType(for: unverifiedVaultConfig) { + case .hub: + self.showHubLoginScreen(vaultConfig: unverifiedVaultConfig, domain: domain) + case .masterkeyFile: + let viewModel = UnlockVaultViewModel(domain: domain, wrongBiometricalPassword: unlockError == .biometricalUnlockWrongPassword) + self.showManualPasswordScreen(viewModel: viewModel) + case .unknown: + fatalError("TODO: throw error") + } + } else { + let viewModel = UnlockVaultViewModel(domain: domain, wrongBiometricalPassword: unlockError == .biometricalUnlockWrongPassword) + self.showManualPasswordScreen(viewModel: viewModel) + } + }.catch { + self.handleError($0) + } + } + + func showHubLoginScreen(vaultConfig: UnverifiedVaultConfig, domain: NSFileProviderDomain) { + let child = HubXPCLoginCoordinator(navigationController: navigationController, + domain: domain, + vaultConfig: vaultConfig, + onUnlocked: { [weak self] in self?.done() }, + onErrorAlertDismissed: { [weak self] in self?.done() }) + childCoordinators.append(child) + child.start() + } + func showManualPasswordScreen(viewModel: UnlockVaultViewModel) { let unlockVaultVC = UnlockVaultViewController(viewModel: viewModel) unlockVaultVC.coordinator = self @@ -129,4 +185,11 @@ class FileProviderCoordinator { hostViewController.view.addSubview(viewController.view) viewController.didMove(toParent: hostViewController) } + + private func handleError(_ error: Error) { + guard let hostViewController = hostViewController else { + return + } + handleError(error, for: hostViewController) + } } diff --git a/FileProviderExtensionUI/RootViewController.swift b/FileProviderExtensionUI/RootViewController.swift index 2da0ac122..99f427bbf 100644 --- a/FileProviderExtensionUI/RootViewController.swift +++ b/FileProviderExtensionUI/RootViewController.swift @@ -10,6 +10,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore import CryptomatorFileProvider +import Dependencies import FileProviderUI import MSAL import Promises @@ -24,6 +25,8 @@ class RootViewController: FPUIActionExtensionViewController { #endif }() + @Dependency(\.fileProviderConnector) private var fileProviderConnector + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) NotificationCenter.default.addObserver(self, @@ -56,22 +59,7 @@ class RootViewController: FPUIActionExtensionViewController { static var oneTimeSetup: () -> Void = { // Set up logger LoggerSetup.oneTimeSetup() - // Set up database - guard let dbURL = CryptomatorDatabase.sharedDBURL else { - // MARK: Handle error - - DDLogError("dbURL is nil") - return {} - } - do { - let dbPool = try CryptomatorDatabase.openSharedDatabase(at: dbURL) - CryptomatorDatabase.shared = try CryptomatorDatabase(dbPool) - } catch { - // MARK: Handle error - DDLogError("Initializing CryptomatorDatabase failed with error: \(error)") - return {} - } // Set up cloud storage services CloudProviderDBManager.shared.useBackgroundSession = false DropboxSetup.constants = DropboxSetup(appKey: CloudAccessSecrets.dropboxAppKey, sharedContainerIdentifier: nil, keychainService: CryptomatorConstants.mainAppBundleId, forceForegroundSession: true) @@ -87,7 +75,7 @@ class RootViewController: FPUIActionExtensionViewController { }() func retryUpload(for itemIdentifiers: [NSFileProviderItemIdentifier], domainIdentifier: NSFileProviderDomainIdentifier) { - let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .uploadRetryingService, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .uploadRetryingService, domainIdentifier: domainIdentifier) getXPCPromise.then { xpc in return wrap { xpc.proxy.retryUpload(for: itemIdentifiers, reply: $0) @@ -100,8 +88,8 @@ class RootViewController: FPUIActionExtensionViewController { }.catch { error in DDLogError("Retry upload failed with error: \(error)") self.extensionContext.cancelRequest(withError: NSError(domain: FPUIErrorDomain, code: Int(FPUIExtensionErrorCode.failed.rawValue), userInfo: nil)) - }.always { - FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + }.always { [fileProviderConnector] in + fileProviderConnector.invalidateXPC(getXPCPromise) } } @@ -113,7 +101,7 @@ class RootViewController: FPUIActionExtensionViewController { } func showUploadProgressAlert(for itemIdentifiers: [NSFileProviderItemIdentifier], domainIdentifier: NSFileProviderDomainIdentifier) { - let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .uploadRetryingService, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .uploadRetryingService, domainIdentifier: domainIdentifier) let progressAlert = RetryUploadAlertControllerFactory.createUploadProgressAlert(dismissAction: { [weak self] in self?.cancel() }, retryAction: { [weak self] in @@ -123,9 +111,9 @@ class RootViewController: FPUIActionExtensionViewController { let observeProgressPromise = progressAlert.observeProgress(itemIdentifier: itemIdentifiers[0], proxy: xpc.proxy) let alertActionPromise = progressAlert.alertActionTriggered return race([observeProgressPromise, alertActionPromise]) - }.always { + }.always { [fileProviderConnector] in self.extensionContext.completeRequest() - FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + fileProviderConnector.invalidateXPC(getXPCPromise) } present(progressAlert, animated: true) } @@ -150,7 +138,7 @@ class RootViewController: FPUIActionExtensionViewController { } func evictFilesFromCache(with itemIdentifiers: [NSFileProviderItemIdentifier], domainIdentifier: NSFileProviderDomainIdentifier) { - let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .cacheManaging, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .cacheManaging, domainIdentifier: domainIdentifier) getXPCPromise.then { xpc in xpc.proxy.evictFilesFromCache(with: itemIdentifiers) }.catch { error in @@ -165,8 +153,8 @@ class RootViewController: FPUIActionExtensionViewController { self.present(alertController, animated: true) }.then { self.extensionContext.completeRequest() - }.always { - FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + }.always { [fileProviderConnector] in + fileProviderConnector.invalidateXPC(getXPCPromise) } } diff --git a/FileProviderExtensionUI/UnlockVaultViewModel.swift b/FileProviderExtensionUI/UnlockVaultViewModel.swift index 10b46d3aa..5e4a87c5f 100644 --- a/FileProviderExtensionUI/UnlockVaultViewModel.swift +++ b/FileProviderExtensionUI/UnlockVaultViewModel.swift @@ -11,6 +11,7 @@ import CryptomatorCloudAccessCore import CryptomatorCommonCore import CryptomatorCryptoLib import CryptomatorFileProvider +import Dependencies import FileProvider import FileProviderUI import Foundation @@ -106,7 +107,7 @@ class UnlockVaultViewModel { } }() - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector private let vaultAccountManager: VaultAccountManager private let providerManager: CloudProviderManager private let vaultCache: VaultCache @@ -115,17 +116,15 @@ class UnlockVaultViewModel { public convenience init(domain: NSFileProviderDomain, wrongBiometricalPassword: Bool) { self.init(domain: domain, wrongBiometricalPassword: wrongBiometricalPassword, - fileProviderConnector: FileProviderXPCConnector.shared, passwordManager: VaultPasswordKeychainManager(), vaultAccountManager: VaultAccountDBManager.shared, providerManager: CloudProviderDBManager.shared, - vaultCache: VaultDBCache(dbWriter: CryptomatorDatabase.shared.dbPool)) + vaultCache: VaultDBCache()) } - init(domain: NSFileProviderDomain, wrongBiometricalPassword: Bool, fileProviderConnector: FileProviderConnector, passwordManager: VaultPasswordManager, vaultAccountManager: VaultAccountManager, providerManager: CloudProviderManager, vaultCache: VaultCache) { + init(domain: NSFileProviderDomain, wrongBiometricalPassword: Bool, passwordManager: VaultPasswordManager, vaultAccountManager: VaultAccountManager, providerManager: CloudProviderManager, vaultCache: VaultCache) { self.domain = domain self.wrongBiometricalPassword = wrongBiometricalPassword - self.fileProviderConnector = fileProviderConnector let context = LAContext() if #unavailable(iOS 16) { // Remove fallback title because "Enter password" also closes FileProviderExtensionUI (prior to iOS 16) and does not display the password input diff --git a/SharedResources/ar.lproj/Localizable.strings b/SharedResources/ar.lproj/Localizable.strings index ca085911a..d277bdefa 100644 --- a/SharedResources/ar.lproj/Localizable.strings +++ b/SharedResources/ar.lproj/Localizable.strings @@ -7,9 +7,10 @@ "common.alert.error.title" = "خطأ"; "common.alert.attention.title" = "انتباه"; -"common.button.cancel" = "الغاء"; +"common.button.cancel" = "إلغاء"; "common.button.change" = "تغيير"; "common.button.choose" = "اختر"; +"common.button.clear" = "مسح"; "common.button.close" = "إغلاق"; "common.button.confirm" = "تأكيد"; "common.button.create" = "إنشاء"; @@ -20,6 +21,7 @@ "common.button.enable" = "تفعيل"; "common.button.next" = "التالي"; "common.button.ok" = "موافق"; +"common.button.refresh" = "تحديث"; "common.button.remove" = "حذف"; "common.button.retry" = "اعد المحاولة"; "common.button.signOut" = "تسجيل الخروج"; @@ -29,6 +31,7 @@ "common.cells.url" = "الرابط"; "common.cells.username" = "اسم المستخدم"; "common.footer.learnMore" = "معرفة المزيد."; +"common.hud.authenticating" = "المصادقة…"; "accountList.header.title" = "المصادقة"; "accountList.emptyList.message" = "انقر هنا لإضافة حساب"; @@ -38,7 +41,7 @@ "addVault.title" = "أضِف مخزنًا"; "addVault.createNewVault.title" = "إنشاء مخزن جديد"; "addVault.createNewVault.setVaultName.header.title" = "اختر اسماً للمخزن."; -"addVault.createNewVault.setVaultName.cells.name" = "اسم المخزن"; +"addVault.createNewVault.setVaultName.cells.name" = "اسم الخزينة"; "addVault.createNewVault.setVaultName.error.emptyVaultName" = "اسم المخزن لا يمكن أن يكون فارغاً."; "addVault.createNewVault.chooseCloud.header" = "أين يجب على Cryptomator تخزين الملفات المشفرة للمخزن الخاص بك؟"; "addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" موجود مسبقاً في هذا الموقع. اختر اسم مخزن أو موقع مختلف."; @@ -95,6 +98,13 @@ "fileProvider.error.biometricalAuthWrongPassword.message" = "كلمة المرور التي تم حفظها لـ %@ خاطئة. الرجاء المحاولة مرة أخرى وإدخال كلمة المرور الخاصة بك لإعادة تمكين %@."; "fileProvider.error.defaultLock.title" = "يلزم فك القفل"; "fileProvider.error.unlockButton" = "افتح"; +"fileProvider.uploadProgress.connecting" = "جاري الاتصال…"; +"hubAuthentication.accessNotGranted" = "لم يتم بعد منح الإذن لجهازك بالوصول إلى هذا المخزن. اطلب من مالك المخزن أن يأذن بذلك."; +"hubAuthentication.licenseExceeded" = "نموذج المركز Cryptomator الخاص بك لديه ترخيص غير صالح. الرجاء إبلاغ مسؤول مركز لترقية أو تجديد الترخيص."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "اسم الجهاز"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "للدخول إلى الخزينة، يحتاج جهازك إلى إذن من مالك الخزينة."; +"keepUnlockedDuration.auto.shortDisplayName" = "تلقائيًا"; +"keepUnlockedDuration.indefinite" = "دائم"; "localFileSystemAuthentication.createNewVault.header" = "في الشاشة التالية، اختر موقع التخزين للمخزن الجديد الخاص بك."; "localFileSystemAuthentication.createNewVault.button" = "حدد موقع التخزين"; @@ -104,7 +114,24 @@ "localFileSystemAuthentication.openExistingVault.error.noVaultFound" = "المجلد المحدد ليس مخزن. الرجاء المحاولة مرة أخرى مع مجلد مختلف."; "onboarding.title" = "أهلاً وسهلاً"; +"onboarding.button.continue" = "استمرار"; +"purchase.footer.privacyPolicy" = "سياسة الخصوصية"; +"purchase.footer.termsOfUse" = "شروط الإستخدام"; +"purchase.header.feature.familySharing" = "مشاركة عائلية"; +"purchase.header.feature.openSource" = "تطوير مفتوح المصدر"; +"purchase.product.donateAndUpgrade" = "تبرع و ترقية"; +"purchase.product.freeUpgrade" = "ترقية مجانية"; +"purchase.product.lifetimeLicense" = "رخصة لمدى الحياة"; +"purchase.product.lifetimeLicense.duration" = "لمرة واحدة"; +"purchase.product.pricing.free" = "مجاناً"; +"purchase.product.trial" = "تجربة مجانية لمدة 30 يوم"; +"purchase.product.trial.expirationDate" = "تاريخ إنتهاء الصلاحية: %@"; +"purchase.product.trial.duration" = "لمدة 30 يوماً"; +"purchase.product.yearlySubscription.duration" = "سنوياً"; +"purchase.readOnlyMode.alert.title" = "وضع القراءة فقط"; +"purchase.restorePurchase.button" = "استعادة المشتريات"; "purchase.retry.button" = "اعد المحاولة"; +"purchase.unlockedFullVersion.title" = "شكراً لك"; "settings.title" = "الإعدادات"; "settings.aboutCryptomator.title" = "الإصدار %@ (%@)"; @@ -112,8 +139,10 @@ "settings.clearCache" = "مسح بيانات الذاكرة المؤقتة"; "settings.cloudServices" = "الخدمات السحابية"; "settings.debugMode.alert.message" = "في هذا الوضع، يمكن كتابة البيانات الحساسة إلى ملف تسجيل على جهازك (على سبيل المثال أسماء الملفات والمسارات). كلمة المرور، ملفات تعريف الارتباط، إلخ. مستبعدة صراحة.\n\nتذكر تعطيل وضع التصحيح في أقرب وقت ممكن."; +"settings.manageSubscriptions" = "إدارة الاشتراك"; "settings.rateApp" = "تقييم التطبيق"; "settings.sendLogFile" = "إرسال ملف السجل"; +"settings.shortcutsGuide" = "دليل الاختصارات"; "settings.unlockFullVersion" = "فتح النسخة الكاملة"; "s3Authentication.displayName" = "الاسم المعروض"; @@ -122,6 +151,10 @@ "s3Authentication.existingBucket" = "مخزن موجود مسبقاً"; "s3Authentication.endpoint" = "نقطة الوصول"; "s3Authentication.region" = "المنطقة"; +"s3Authentication.error.invalidCredentials" = "معلومات الدخول خاطئة."; + +"trialStatus.active" = "مفعل"; +"trialStatus.expired" = "منتهية الصلاحية"; "unlockVault.button.unlock" = "افتح"; "unlockVault.button.unlockVia" = "فتح عبر %@"; @@ -146,12 +179,17 @@ "vaultDetail.button.changeVaultPassword" = "تغيير كلمة المرور"; "vaultDetail.button.lock" = "قفل الآن"; "vaultDetail.button.moveVault" = "نقل"; +"vaultDetail.button.removeVault" = "إزالة من قائمة المخازن"; "vaultDetail.button.renameVault" = "إعادة تسمية"; "vaultDetail.disabledBiometricalUnlock.footer" = "إذا مكّنت %@، سيتم تخزين كلمة المرور الخاصة بك في سلسلة مفاتيح iOS."; +"vaultDetail.keepUnlocked.title" = "مدة الفتح"; +"vaultDetail.moveVault.progress" = "جاري النقل…"; +"vaultDetail.renameVault.progress" = "إعادة تسمية…"; "vaultDetail.unlockVault.footer" = "أدخل كلمة المرور ل \"%@\" لتخزينها في سلسلة مفاتيح iOS وتمكين %@."; "vaultList.header.title" = "المخازن"; "vaultList.emptyList.message" = "انقر هنا لإضافة مخزن"; +"vaultList.remove.alert.title" = "حذف المخزن؟"; "webDAVAuthentication.httpConnection.alert.title" = "إستخدام HTTPS؟"; "webDAVAuthentication.httpConnection.alert.message" = "استخدام HTTP غير آمن. نوصي باستخدام HTTPS بدلاً من ذلك. إذا كنت تعرف المخاطر، يمكنك الاستمرار مع HTTP."; diff --git a/SharedResources/ba.lproj/Localizable.strings b/SharedResources/ba.lproj/Localizable.strings new file mode 100644 index 000000000..0e23544b7 --- /dev/null +++ b/SharedResources/ba.lproj/Localizable.strings @@ -0,0 +1,305 @@ +/* + Localizable.strings + Cryptomator + + Copyright © 2021 Skymatic GmbH. All rights reserved. +*/ + +"common.alert.error.title" = "Хата"; +"common.alert.attention.title" = "Иғтибар"; +"common.button.cancel" = "Кире ал"; +"common.button.change" = "Үҙгәрт"; +"common.button.choose" = "Һайла"; +"common.button.clear" = "Таҙала"; +"common.button.close" = "Яп"; +"common.button.confirm" = "Раҫла"; +"common.button.create" = "Яһа"; +"common.button.createFolder" = "Каталог өҫтәү"; +"common.button.done" = "Тамам"; +"common.button.download" = "Күсереп ал"; +"common.button.edit" = "Төҙәт"; +"common.button.enable" = "Ғәмәлгә индер"; +"common.button.next" = "Киләһе"; +"common.button.ok" = "Ярай"; +"common.button.refresh" = "Яңырт"; +"common.button.register" = "Теркәл"; +"common.button.remove" = "Алып ташлау"; +"common.button.retry" = "Ҡабатла"; +"common.button.signOut" = "Сыҡ"; +"common.button.verify" = "Тикшер"; +"common.cells.openInFilesApp" = "Файлдар ҡушымтаһында ас"; +"common.cells.password" = "Серһүҙ"; +"common.cells.url" = "URL"; +"common.cells.username" = "Ҡулланыусы исеме"; +"common.footer.learnMore" = "Артабан уҡырға."; +"common.hud.authenticating" = "Аутентиклау…"; + +"accountList.header.title" = "Аутентиклауҙар"; +"accountList.emptyList.message" = "Иҫәп яҙмаһы өҫтәү өсөн бында баҫығыҙ"; +"accountList.signOut.alert.title" = "Бәйле һаҡлағыстарҙы юйырғамы?"; +"accountList.signOut.alert.message" = "Сығыу менән бөтә бәйле һаҡлағыстар исемлектән алып ташланасаҡ. Шифрланған мәғлүмәттәр юйылмаясаҡ. Аҙаҡтан яңынан инеп һаҡлағыстарҙы ҡабаттан өҫтәй алаһығыҙ."; + +"addVault.title" = "Һаҡлағыс өҫтәү"; +"addVault.createNewVault.title" = "Яңы һаҡлағыс яһау"; +"addVault.createNewVault.purchase" = "Яңы һаҡлағыс яһау өсөн Cryptomator-ҙың тулы версияһы кәрәк."; +"addVault.createNewVault.setVaultName.header.title" = "Һаҡлағысҡа исем һайлағыҙ."; +"addVault.createNewVault.setVaultName.cells.name" = "Һаҡлағыс исеме"; +"addVault.createNewVault.setVaultName.error.emptyVaultName" = "Һаҡлағыс исеме буш була алмай."; +"addVault.createNewVault.chooseCloud.header" = "Cryptomator һаҡлағысығыҙҙың шифрланған файлдарын ҡайҙа һаҡларға тейеш?"; +"addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" был урында бар инде. Башҡа һаҡлағыс исеме йәки урын һайлағыҙ."; +"addVault.createNewVault.detectedMasterkey.text" = "Cryptomator был урында булған һаҡлағыс тапты.\nЯңы һаҡлағыс булдырыу өсөн кире ҡайтып икенсе каталог һайлағыҙ."; +"addVault.createNewVault.password.enterPassword.header" = "Яңы серһүҙ яҙығыҙ."; +"addVault.createNewVault.password.confirmPassword.header" = "Яңы серһүҙҙе раҫлағыҙ."; +"addVault.createNewVault.password.confirmPassword.alert.title" = "Серһүҙҙе раҫлайһығыҙмы?"; +"addVault.createNewVault.password.confirmPassword.alert.message" = "МӨҺИМ: әгәр серһүҙҙе онотһағыҙ, уны тергеҙеү ысулы юҡ."; +"addVault.createNewVault.password.error.emptyPassword" = "Серһүҙ буш була алмай."; +"addVault.createNewVault.password.error.nonMatchingPasswords" = "Серһүҙҙәр тап килмәй."; +"addVault.createNewVault.password.error.tooShortPassword" = "Серһүҙ кәмендә 8 билдәнән торорға тейеш."; +"addVault.createNewVault.progress" = "Һаҡлағыс яһау…"; +"addVault.openExistingVault.title" = "Булған һаҡлағысты асыу"; +"addVault.openExistingVault.chooseCloud.header" = "Һаҡлағыс урыны ҡайҙа?"; +"addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator \"%@\" һаҡлағысын тапты.\nБыл һаҡлағысты өҫтәргә теләйһегеҙме?"; +"addVault.openExistingVault.detectedMasterkey.add" = "Был һаҡлағысты өҫтә"; +"addVault.openExistingVault.downloadVault.progress" = "Һаҡлағыс күсерелә…"; +"addVault.openExistingVault.password.footer" = "\"%@\" серһүҙен яҙығыҙ."; +"addVault.openExistingVault.progress" = "Һаҡлағыс өҫтәү…"; +"addVault.success.info" = "\"%@\" һаҡлағысы өҫтәлде. \nФайлдар ҡушымтаһы аша һаҡлағысҡа инә алаһығыҙ."; +"addVault.success.footer" = "Быға тиклем Cryptomator-ҙы ғәмәлгә индермәгән булһағыҙ, уны Файлдар ҡушымтаһы аша эшләп була."; + +"biometryType.faceID" = "Face ID"; +"biometryType.touchID" = "Touch ID"; + +"changePassword.error.invalidOldPassword" = "Хәҙерге серһүҙ дөрөҫ түгел. Яңынан ҡабатлағыҙ."; +"changePassword.header.currentPassword.title" = "Хәҙерге серһүҙҙе яҙығыҙ."; +"changePassword.header.newPassword.title" = "Яңы серһүҙ яҙығыҙ."; +"changePassword.header.newPasswordConfirmation.title" = "Яңы серһүҙҙе раҫлағыҙ."; +"changePassword.progress" = "Серһүҙҙе үҙгәртеү…"; + +"chooseFolder.emptyFolder.footer" = "Каталог буш"; +"chooseFolder.createNewFolder.header.title" = "Каталог өсөн исем һайлағыҙ."; +"chooseFolder.createNewFolder.cells.name" = "Каталог исеме"; +"chooseFolder.createNewFolder.error.emptyFolderName" = "Каталог исеме буш була алмай."; +"chooseFolder.createNewFolder.progress" = "Каталог яһау…"; + +"cloudProvider.error.itemNotFound" = "\"%@\" табылманы."; +"cloudProvider.error.itemAlreadyExists" = "\"%@\" бар инде."; +"cloudProvider.error.itemTypeMismatch" = "\"%@\" көтөлмәгән элемент төрөнә эйә."; +"cloudProvider.error.parentFolderDoesNotExist" = "Төп каталог \"%@\" юҡ."; +"cloudProvider.error.pageTokenInvalid" = "Каталогтың йөкмәткеһен алыуҙы дауам итеп булмай."; +"cloudProvider.error.quotaInsufficient" = "Һаҡлағысығыҙҙа урын етмәй."; +"cloudProvider.error.unauthorized" = "Рөхсәтһеҙ ғәмәлде башҡарып булмай."; +"cloudProvider.error.noInternetConnection" = "Был ғәмәл өсөн интернет бәйләнеше кәрәк."; + +"cloudProviderType.localFileSystem" = "Башҡа файл провайдеры"; + +"fileProvider.onboarding.title" = "Рәхим итегеҙ"; +"fileProvider.onboarding.info" = "Файлдарығыҙҙы һаҡлау өсөн Cryptomator һайлағанығыҙ өсөн рәхмәт. Башлар өсөн төп ҡушымтаға инеп һаҡлағыс өҫтәгеҙ."; +"fileProvider.onboarding.button.openCryptomator" = "Cryptomator ас"; +"fileProvider.error.biometricalAuthCanceled.title" = "Биген асыу туҡтатылды"; +"fileProvider.error.biometricalAuthCanceled.message" = "%@ аша бик асыу уңышһыҙ булды. Яңынан ҡабатлап ҡарағыҙ."; +"fileProvider.error.biometricalAuthWrongPassword.title" = "Серһүҙ дөрөҫ түгел"; +"fileProvider.error.biometricalAuthWrongPassword.message" = "%@ өсөн һаҡланған серһүҙ дөрөҫ түгел. Ҡабатлап ҡарағыҙ һәм %@ ғәмәлғә инһен өсөн серһүҙҙе яңынан яҙығыҙ."; +"fileProvider.error.defaultLock.title" = "Бикте асыу кәрәкле"; +"fileProvider.error.defaultLock.message" = "Һаҡлағысығыҙға инеү һәм эстәлеген күреү өсөн, уны асырға кәрәк."; +"fileProvider.error.unlockButton" = "Биген ас"; +"fileProvider.clearFileFromCache.title" = "Файлды кэштан таҙалау"; +"fileProvider.clearFileFromCache.message" = "Был йыһазығыҙҙағы урындағы файлды ғына алып ташлай һәм болоттағы файлды юймай."; +"fileProvider.fileImporting.error.missingPremium" = "Һаҡлағыстарығыҙға яҙыу хоҡуғы алыу өсөн Cryptomator ҡушымтаһының тулы версияһын алырға."; +"fileProvider.uploadProgress.connecting" = "Тоташыу…"; +"fileProvider.uploadProgress.message" = "Ағымдағы прогресс:%@\n\nӘгәр йөкләү процесы тотҡарлана икән, йөкләүҙе ҡабаттан башларға мөмкин."; +"fileProvider.uploadProgress.missing" = "Прогресты билдәләп булмай. Артҡы планда һаман эшләп тороуы мөмкин."; +"fileProvider.uploadProgress.title" = "Тейәү…"; +"fileProvider.uploadProgress.missingDomainError" = "Доменды табып булмай."; + +"getFolderIntent.error.missingPath" = "Бер ниндәй ҙә юл бирелмәгән. Каталогты кире ҡайтарыу өсөн дөрөҫ юл биреүегеҙҙе һорайбыҙ."; +"getFolderIntent.error.noVaultSelected" = "Һаҡлағыс һайланмаған."; + +"hubAuthentication.title" = "Һаҡлағыс хабы"; +"hubAuthentication.accessNotGranted" = "Һеҙҙең йыһаз әлегә был һаҡлағысҡа инеү хоҡуғына эйә түгел. Һаҡлағыс хужаһынан рөхсәт һорағыҙ."; +"hubAuthentication.licenseExceeded" = "Һеҙҙең Cryptomator хабығыҙ ғәмәлдән тыш рөхсәтнамәгә эйә. Рөхсәтнамәне яңыртыу йәки оҙайтыу өсөн Хаб администраторына хәбәр итегеҙ."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Йыһаз исеме"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Был, күрәһең, был ҡоролманан Хабҡа тәү инеү ваҡыты. Инеүҙе рөхсәт итеү өсөн йыһазды асыҡлаусы исем атарға кәрәк."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Яңы ҡушымталар йәки браузерҙарҙан инеү өсөн Һеҙҙең иҫәп-хисап асҡысы кәрәк. Уны профилегеҙҙә табырға мөмкин."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Йыһазды теркәү уңышлы булды"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Һаҡлағысҡа инеү өсөн һаҡлағыс хужаһы йыһазығыҙға инеү рөхсәте бирергә тейеш."; +"hubAuthentication.requireAccountInit.alert.title" = "Кәрәкле эш-хәрәкәт"; +"hubAuthentication.requireAccountInit.alert.message" = "Артабан барыр өсөн, Hub ҡулланыусылар профилендә кәрәкле аҙымдарҙы тамамлағыҙ."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Профилгә күс"; + +"intents.saveFile.missingFile" = "Бирелгән файл ғәмәлдә түгел."; +"intents.saveFile.invalidFolder" = "Бирелгән каталог ғәмәлдә түгел."; +"intents.saveFile.missingTemporaryFolder" = "Ваҡытлыса каталогтар яһау уңышһыҙ тамамланды."; +"intents.saveFile.lockedVault" = "Был ҡыҫҡа юлды ҡулланыр өсөн һаҡлағысты асырға кәрәк."; +"intents.saveFile.selectedVaultNotFound" = "Һайланған һаҡлағысты табып булманы."; + +"keepUnlocked.alert.title" = "Һаҡлағысты бикләргәме?"; +"keepUnlocked.alert.message" = "Был үҙгәреш үҙ көсөнә инеү өсөн һаҡлағысты бикләүҙе талап итә."; +"keepUnlocked.alert.confirm" = "Раҫла һәм биклә"; +"keepUnlocked.header" = "Буш торғанда был һаҡлағыстың күпме ваҡыт бикһеҙ ҡалыуын һайлағыҙ."; +"keepUnlocked.footer.auto" = "iOS-ҡа ҡарар ҡабул итеүҙе рөхсәт итеү, хәтерҙе бушатыу өсөн теләһә ҡайһы ваҡытта Cryptomator-ҙы туҡтатырға мөмкин тигәнде аңлата, был һаҡлағысты автоматик рәүештә бикләй."; +"keepUnlocked.footer.on" = "Һайланған вариант ҡулланылғанда, һаҡлағыстың биге асылған саҡта, асҡыстың күсермәһе iOS-тың асҡыс сылбырында һаҡланырға тейеш."; +"keepUnlockedDuration.auto" = "iOS автоматик рәүештә ҡарар ҡабул итһен"; +"keepUnlockedDuration.auto.shortDisplayName" = "Авто"; +"keepUnlockedDuration.indefinite" = "Билдәһеҙ"; + +"localFileSystemAuthentication.createNewVault.header" = "Киләһе экранда яңы һаҡлағысығыҙ өсөн урын һайлағыҙ."; +"localFileSystemAuthentication.createNewVault.button" = "Һаҡлау урыны һайла"; +"localFileSystemAuthentication.createNewVault.error.detectedExistingVault" = "Был урында һаҡлағыс бар инде. Башҡа һаҡлау урыны менән ҡабатлап ҡарағыҙ."; +"localFileSystemAuthentication.openExistingVault.header" = "Киләһе экранда булған һаҡлағысығыҙ өсөн каталог һайлағыҙ."; +"localFileSystemAuthentication.openExistingVault.button" = "Һаҡлағыс каталогы һайла"; +"localFileSystemAuthentication.openExistingVault.error.noVaultFound" = "Һайланған каталог һаҡлағыс түгел. Зинһар, икенсе каталог менән ҡабатлап ҡарағыҙ."; +"localFileSystemAuthentication.info.footer" = "Һоро төҫтәге файл провайдерҙары «каталогтар йыйыуҙы» хупламай. Был Cryptomator сикләүе түгел."; + +"maintenanceModeError.runningCloudTask" = "Ғәмәлде башҡарып булмай сөнки башта был һаҡлағыстың башҡа ғәмәлдәре тамамланырға тейеш. Һуңыраҡ ҡабатлап ҡарағыҙ."; + +"nameValidation.error.endsWithPeriod" = "Нөктә менән тамамланған исемде ҡулланырға ярамай. Икенсе исем һайлағыҙ."; +"nameValidation.error.endsWithSpace" = "Арауыҡ менән тамамланған исемде ҡулланырға ярамай. Икенсе исем һайлағыҙ."; +"nameValidation.error.containsIllegalCharacter" = "\"%@\" булған исемде ҡулланырға ярамай. Икенсе исем һайлағыҙ."; + +"onboarding.title" = "Рәхим итегеҙ"; +"onboarding.info" = "Файлдарығыҙҙы һаҡлау өсөн Cryptomator һайлағанығыҙ өсөн рәхмәтn\n\nCryptomator менән мәғлүмәттәрегеҙҙең асҡысы — һеҙҙең ҡулда. Cryptomator мәғлүмәттәрегеҙҙе тиҙ һәм еңел шифрлай.\n\nБыл ҡушымта тулыһынса Файлдар ҡушымтаһы менән берләштерелгән. Һуңыраҡ Файлдар ҡушымтаһында Cryptomator'ға һаҡлағыстарығыҙға инеү мөмкинлеген бирегеҙ."; +"onboarding.button.continue" = "Дауам ит"; + +"purchase.beginFreeTrial.alert.title" = "Һынау версияһы асылды"; +"purchase.expiredTrial" = "Һынау вакыты бөттө."; +"purchase.footer.privacyPolicy" = "Хосусилыҡ сәйәсәте"; +"purchase.footer.termsOfUse" = "Ҡулланыу шарттары"; +"purchase.header.feature.familySharing" = "Ғаилә менән уртаҡлашыу"; +"purchase.header.feature.openSource" = "Open-source эшләү"; +"purchase.header.feature.writeAccess" = "Һаҡлағыстарға яҙыу рөхсәте"; +"purchase.product.donateAndUpgrade" = "Иғәнә һәм яңыртыу"; +"purchase.product.freeUpgrade" = "Түләүһеҙ яңыртыу"; +"purchase.product.lifetimeLicense" = "Ғүмерлек рөхсәтнамә"; +"purchase.product.lifetimeLicense.duration" = "бер тапҡыр"; +"purchase.product.pricing.free" = "Түләүһеҙ"; +"purchase.product.trial" = "30 көнлөк һынау"; +"purchase.product.trial.expirationDate" = "Ҡулланыу ваҡыты: %@"; +"purchase.product.trial.duration" = "30 көн"; +"purchase.product.yearlySubscription" = "Йыллыҡ яҙылыу"; +"purchase.product.yearlySubscription.duration" = "йыл һайын"; +"purchase.readOnlyMode.alert.title" = "Уҡыу ғына режимы"; +"purchase.readOnlyMode.alert.message" = "Cryptomator ҡушымтаһының тулы версияһын һуңыраҡ көйләүҙәрҙә аса һәм уны хәҙергә тик уҡыу режимында ғына ҡуллана алаһығыҙ."; +"purchase.restorePurchase.button" = "Һатып алыуҙы тергеҙеү"; +"purchase.restorePurchase.validTrialFound.alert.title" = "Һынау дауам ителде"; +"purchase.restorePurchase.validTrialFound.alert.message" = "Cryptomator-ҙың тулы версияһын хәҙер сикләүле ваҡытта ҡулланырға мөмкин. Һеҙҙең һынау ваҡыты %@- тамамлана. Шунан һуң да һаҡлағыстарығыҙға бары тик уҡыу режимында ғына инергә мөмкин буласаҡ."; +"purchase.restorePurchase.fullVersionFound.alert.title" = "Тергеҙеү уңышлы булды"; +"purchase.restorePurchase.fullVersionNotFound.alert.title" = "Тулы версия табылманы"; +"purchase.restorePurchase.fullVersionNotFound.alert.message" = "Элек һатып алынған, тергеҙелә алырлыҡ тулы версияны таба алманыҡ. Зинһар, икенсе вариантты һынап ҡарағыҙ."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Яңыртыу хоҡуғына эйә"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Cryptomator-ҙың иҫкерәк версияһынан яңыртырға тырышаһығыҙ шикелле. Был осраҡта уның урынына «Яңыртыу тәҡдиме» вариантын һайлағыҙ."; +"purchase.retry.button" = "Ҡабатла"; +"purchase.retry.footer" = "Булған продукттарҙы тейәп булмай."; +"purchase.title" = "Тулы версияны асыу"; +"purchase.unlockedFullVersion.message" = "Хәҙер һеҙ Cryptomator-ҙың тулы версияһын ҡуллана алаһығыҙ. Хәйерле шифрлауҙар!"; +"purchase.unlockedFullVersion.title" = "Рәхмәт"; +"purchase.error.unknown" = "Был ҡушымта билдәһеҙ сәбәптәр арҡаһында App Store-ҙа юҡ. Зинһар, һуңыраҡ ҡабатлап ҡарағыҙ.\n\nӘгәр был хата ҡабатланһа, йыһазды яңынан эшләтеп ҡарағыҙ йәки iOS көйләүҙәрендә Apple ID-ғыҙҙан сығығыҙ һәм кире керегеҙ."; + +"settings.title" = "Көйләүҙәр"; +"settings.aboutCryptomator" = "Cryptomator тураһында"; +"settings.aboutCryptomator.title" = "%@ версияһы (%@)"; +"settings.cacheSize" = "Кэш күләме"; +"settings.clearCache" = "Кэшты таҙартыу"; +"settings.cloudServices" = "Болот хеҙмәттәре"; +"settings.contact" = "Бәйләнеш"; +"settings.debugMode" = "Төҙөкләндереү режимы"; +"settings.debugMode.alert.message" = "Был режимда нескә мәғлүмәттәрҙең (мәҫәлән, файл исемдәре, юлдар) йыһаздың журнал файлына яҙылыуы ихтимал. Серһүҙҙәр, cookie-файлдар һ.б. тура алып ташлана.\n\nЯйлау (debug) режимын тиҙ арала ябырға онотмағыҙ."; +"settings.manageSubscriptions" = "Яҙылыуҙар менән идара итеү"; +"settings.rateApp" = "Ҡушымтаны баһала"; +"settings.sendLogFile" = "Журнал файлын ебәр"; +"settings.shortcutsGuide" = "Ҡыҫҡа юлдар гиды"; +"settings.unlockFullVersion" = "Тулы версияны асыу"; + +"snapshots.fileprovider.file1" = "/Иҫәп-хисап.numbers"; +"snapshots.fileprovider.file2" = "/Һуңғы презентация.key"; +"snapshots.fileprovider.file3" = "/Продукт трейлеры.mov"; +"snapshots.fileprovider.file4" = "/Тәҡдим.docx"; +"snapshots.fileprovider.file5" = "/Хисаплама.pdf"; +"snapshots.fileprovider.folder3" = "/Серле проект"; +"snapshots.fileprovider.folder2" = "Фактуралар"; +"snapshots.fileprovider.folder1" = "/Сертификаттар"; +"snapshots.main.vault1" = "/Эш"; +"snapshots.main.vault2" = "/Ғаилә"; +"snapshots.main.vault3" = "/Документтар"; +"snapshots.main.vault4" = "/Калифорния сәйәхәте"; + +"s3Authentication.displayName" = "Күренәсәк исем"; +"s3Authentication.accessKey" = "Инеү асҡысы (Access Key)"; +"s3Authentication.secretKey" = "Серле асҡыс (Secret Key)"; +"s3Authentication.existingBucket" = "Булған кәрзин (Bucket)"; +"s3Authentication.endpoint" = "Ос нөктә (Endpoint)"; +"s3Authentication.region" = "Регион"; +"s3Authentication.error.invalidCredentials" = "Хаталы иҫәп яҙмаһы мәғлүмәттәре."; +"s3Authentication.error.invalidEndpoint" = "Бирелгән ос нөктә URL-адресы форматына тап килмәй."; + +"trialStatus.active" = "Ғәмәлдә"; +"trialStatus.expired" = "Ваҡыты үткән"; + +"unlockVault.button.unlock" = "Биген ас"; +"unlockVault.button.unlockVia" = "%@ аша биген ас"; +"unlockVault.password.footer" = "\"%@\" серһүҙен яҙығыҙ."; +"unlockVault.enableBiometricalUnlock.switch" = "%@ ғәмәлдә"; +"unlockVault.enableBiometricalUnlock.footer" = "Һаҡлағысты серһүҙ менән асыу урынына уны %@ аша асырға мөмкин."; +"unlockVault.evaluatePolicy.reason" = "Һаҡлағысығыҙҙы асыу"; +"unlockVault.progress" = "Бикте асыу…"; + +"untrustedTLSCertificate.title" = "Ғәмәлдә булмаған TLS сертификаты"; +"untrustedTLSCertificate.message" = "\"%@\" TLS сертификаты ғәмәлдә түгел. Нисек булһа ла, уға ышанырға теләйһегеҙме?\n\nSHA-256: %@"; +"untrustedTLSCertificate.add" = "Ышан"; +"untrustedTLSCertificate.dismiss" = "Ышанмаҫҡа"; + +"upgrade.title" = "Яңыртыу тәҡдиме"; +"upgrade.notEligible.alert.title" = "Яңыртыу уңышһыҙ булды"; +"upgrade.notEligible.alert.message" = "Cryptomator йыһазығыҙға ҡуйылған элекке версияны таба алманы. Әгәр уны һатып алған булһағыҙ, App Store-ҙан яңынан күсереп алып һәм яңынан ҡабатлағыҙ."; +"upgrade.info" = "Беренсе версиянан Cryptomator-ға ышаныуығыҙ өсөн рәхмәт. Тоғро ҡулланыусы булараҡ, һеҙ бушлай яңыртыу хоҡуғына эйә."; + +"urlSession.error.httpError.401" = "Хаталы ҡулланыусы исеме һәм/йәки серһүҙ."; +"urlSession.error.httpError.403" = "Һоралған ресурсҡа хоҡуҡтар етмәй."; +"urlSession.error.httpError.404" = "Һоралған ресурс табылманы."; +"urlSession.error.httpError.405" = "Һоратыу методы маҡсат ресурс тарафынан терәкләнмәй."; +"urlSession.error.httpError.409" = "Һоратыу маҡсат ресурстың ағымдағы торошона тап килмәй."; +"urlSession.error.httpError.412" = "Маҡсатлы ресурсҡа инеү тыйылды."; +"urlSession.error.httpError.default" = "Селтәр бәйләнеше %ld статус коды менән уышһыҙ булды."; +"urlSession.error.unexpectedResponse" = "Көтөлмәгән селтәр яуабы килде."; + +"vaultAccountManager.error.vaultAccountAlreadyExists" = "Был һаҡлағыс өҫтәлгән инде."; + +"vaultDetail.button.changeVaultPassword" = "Серһүҙҙе үҙгәрт"; +"vaultDetail.button.lock" = "Биклә"; +"vaultDetail.button.moveVault" = "Күсер"; +"vaultDetail.button.removeVault" = "Һаҡлағыс исемлегенән алып ташла"; +"vaultDetail.button.renameVault" = "Исемен үҙгәрт"; +"vaultDetail.changePassword.footer" = "Һаҡлағысығыҙ өсөн үҙегеҙ генә белгән көслө серһүҙ һайлағыҙ һәм уны хәүефһеҙ урында тотоғоҙ."; +"vaultDetail.disabledBiometricalUnlock.footer" = "Әгәр һеҙ %@ ғәмәлдә булһа, һеҙҙең һаҡлағыс серһүҙегеҙ iOS-тың асҡыс сылбырында һаҡланасаҡ."; +"vaultDetail.enabledBiometricalUnlock.footer" = "Һаҡлағыс пароле, %@ аутентиклауы уңышһыҙ булғанда ғына кәрәк буласаҡ."; +"vaultDetail.info.footer.accessVault" = "Файлдар ҡушымтаһы аша һаҡлағысҡа инеү."; +"vaultDetail.info.footer.accountInfo" = "%@ исеме менән %@ аша инелгән."; +"vaultDetail.keepUnlocked.title" = "Асыҡ булыу ваҡыты"; +"vaultDetail.keepUnlocked.footer.off" = "Бикте асыу, Cryptomator Файлдар ҡушымтаһы тарафынан туҡтатылған ваҡытта кәрәк буласаҡ."; +"vaultDetail.keepUnlocked.footer.limitedDuration" = "Бикте асыу, һаҡлағыс %@ буш торған ваҡытта кәрәк буласаҡ."; +"vaultDetail.keepUnlocked.footer.unlimitedDuration" = "Ҡул менән бикләнмәһә, асыу талап ителмәйәсәк."; +"vaultDetail.locked.footer" = "Һаҡлағысығыҙ хәҙерге ваҡытта бикле."; +"vaultDetail.moveVault.detectedMasterkey.text" = "Cryptomator был урында булған һаҡлағыс тапты.\nҺаҡлағысты күсереү өсөн кире ҡайтып икенсе каталог һайлағыҙ."; +"vaultDetail.moveVault.progress" = "Күсереү…"; +"vaultDetail.removeVault.footer" = "Был һаҡлағысты исемлектән генә алып ташлай һәм шифрланған файлдарҙы юймай."; +"vaultDetail.renameVault.progress" = "Исемен үҙгәртеү…"; +"vaultDetail.unlocked.footer" = "Әлеге ваҡытта һаҡлағыс Файлдар ҡушымтаһында асылған."; +"vaultDetail.unlockVault.footer" = "\"%@\" серһүҙен iOS асҡыс сылбырында һаҡлау һәм %@ ҡулланыу өсөн серһүҙҙе яҙығыҙ."; + +"vaultList.header.title" = "Һаҡлағыстар"; +"vaultList.emptyList.message" = "Һаҡлағыс өҫтәү өсөн бында баҫығыҙ"; +"vaultList.remove.alert.title" = "Һаҡлағысты алып ташларғамы?"; +"vaultList.remove.alert.message" = "Был һаҡлағысты исемлектән генә алып ташлай. Шифрланған мәғлүмәттәр юйылмай. Һаҡлағысты һуңыраҡ яңынан өҫтәргә мөмкин."; + +"vaultProviderFactory.error.unsupportedVaultConfig" = "Һаҡлағыс конфигурацияһы терәкһеҙ. Cryptomator-ҙың һуңғы версияһын эшләтеүегеҙҙе тикшерегеҙ."; +"vaultProviderFactory.error.unsupportedVaultVersion" = "Һаҡлағыстың терәкһеҙ версияһы: %ld. Был һаҡлағыс Cryptomator-ҙың башҡа версияһы менән яһалған."; + +"webDAVAuthentication.httpConnection.alert.title" = "HTTPS ҡулланырғамы?"; +"webDAVAuthentication.httpConnection.alert.message" = "HTTP ҡулланыу хәүефһеҙ түгел. Уның урынына HTTPS ҡулланырға кәңәш итәбеҙ. Хәүефтәрҙе белһәгеҙ, HTTP менән дауам итә алаһығыҙ."; +"webDAVAuthentication.httpConnection.change" = "HTTPS ҡулланыу"; +"webDAVAuthentication.httpConnection.continue" = "HTTP ҡалһын"; + +"webDAVAuthenticator.error.unsupportedProtocol" = "Сервер WebDAV яраҡлы түгел кеүек. Дөрөҫ URL ҡулланғанығыҙҙы тикшерегеҙ."; +"webDAVAuthenticator.error.untrustedCertificate" = "Был серверҙың сертификаты ышанысһыҙ. Был WebDAV бәйләнешен яңынан өҫтәү кәрәк булыр."; + +"Retry Upload" = "Тейәүҙе ҡабатла"; +"Clear from Cache" = "Кэштан таҙала"; diff --git a/SharedResources/be.lproj/Localizable.strings b/SharedResources/be.lproj/Localizable.strings index 498e79b74..75256fb31 100644 --- a/SharedResources/be.lproj/Localizable.strings +++ b/SharedResources/be.lproj/Localizable.strings @@ -1,12 +1,15 @@ "common.button.cancel" = "Скасаваць"; "common.button.change" = "Змяніць"; +"common.button.choose" = "Абраць"; "common.button.close" = "Зачыніць"; "common.button.create" = "Стварыць"; "common.button.done" = "Файна"; "common.button.edit" = "Рэдагаваць"; "common.button.next" = "Далей"; "common.button.ok" = "Добра"; +"common.button.refresh" = "Абнавiць"; "common.button.remove" = "Выдаліць"; +"common.button.retry" = "Паспрабаваць ізноў"; "common.cells.password" = "Пароль"; "common.cells.url" = "URL"; "common.cells.username" = "Імя карыстальніка"; @@ -17,11 +20,16 @@ "addVault.createNewVault.chooseCloud.header" = "Дзе мусіць Cryptomator захоўваць зашыфраваныя файлы з тваёй скарбніцы?"; "addVault.openExistingVault.title" = "Адчыніць існуючую скарбніцу"; "fileProvider.error.unlockButton" = "Адамкнуць"; +"hubAuthentication.accessNotGranted" = "Тваёй прыладзе ў дадзены момант не дазволена мець доступ да гэтай скрабніцы. Запытайся ўладальніка скрабніцы за дазволам."; +"hubAuthentication.licenseExceeded" = "Твая інстанцыя Cryptomator Hub мае некарэктную ліцэнзію. Калі ласка, паведамі адміністратару Hub пра гэта, каб абнавіць альбо аднавіць ліцэнзію."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Назва прылады"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Каб атрымаць доступ да скарбніцы, твая прылада мусіць быць спраўджанай уладальнікам скарбніцы."; "keepUnlocked.alert.title" = "Ці замкнуць скарбніцу?"; "keepUnlocked.alert.confirm" = "Пацвердзіць і адразу замкнуць"; "keepUnlockedDuration.auto" = "Дазволіць iOS прымаць рашэнне самастойна"; "keepUnlockedDuration.auto.shortDisplayName" = "Аўтаматычна"; +"purchase.retry.button" = "Паспрабаваць ізноў"; "purchase.unlockedFullVersion.title" = "Шчыры дзякуй"; "settings.title" = "Налады"; @@ -34,5 +42,6 @@ "unlockVault.button.unlock" = "Адамкнуць"; "vaultDetail.button.changeVaultPassword" = "Змяніць пароль"; +"vaultDetail.button.moveVault" = "Перамясціць"; "vaultDetail.button.renameVault" = "Пераназваць"; "vaultDetail.keepUnlocked.title" = "Працягласць размыкання"; diff --git a/SharedResources/bg.lproj/Localizable.strings b/SharedResources/bg.lproj/Localizable.strings new file mode 100644 index 000000000..5dce12828 --- /dev/null +++ b/SharedResources/bg.lproj/Localizable.strings @@ -0,0 +1,52 @@ +"common.button.cancel" = "Отказ"; +"common.button.change" = "Променяне"; +"common.button.choose" = "Избор"; +"common.button.close" = "Затваряне"; +"common.button.create" = "Създаване"; +"common.button.done" = "Готово"; +"common.button.edit" = "Редактиране"; +"common.button.next" = "Напред"; +"common.button.refresh" = "Презареждане"; +"common.button.register" = "Регистриране"; +"common.button.remove" = "Премахване"; +"common.button.retry" = "Повторен опит"; +"common.cells.password" = "Парола"; +"common.cells.url" = "URL"; +"common.cells.username" = "Потребител"; + +"addVault.title" = "Добавяне на хранилище"; +"addVault.createNewVault.title" = "Ново хранилище"; +"addVault.createNewVault.setVaultName.cells.name" = "Наименование"; +"addVault.createNewVault.chooseCloud.header" = "Къде Криптоматор ще държи шифрованите файлове на хранилището?"; +"addVault.createNewVault.password.confirmPassword.alert.message" = "ВАЖНО: Ако забравите паролата няма начин да възстановите достъпа до данните."; +"addVault.openExistingVault.title" = "Отваряне на хранилище"; +"addVault.openExistingVault.downloadVault.progress" = "Изтегля се хранилище…"; +"fileProvider.error.unlockButton" = "Отключване"; + +"hubAuthentication.title" = "Хранилище на Hub"; +"hubAuthentication.accessNotGranted" = "Устройството не е упълномощено за достъп до това хранилище. Поискайте достъп от собственика."; +"hubAuthentication.licenseExceeded" = "Лиценза на екземпляра на Концентратора на Криптоматор който вие използвате е лиценз. Информирайте администратора на Концентратора, за да поднови или надгради лиценза."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Име на устройството"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Изглежда, че това е първи достъп до Hub от това устройство. За да го разпознаете при разрешаване на достъпа, трябва да му дадете име."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Вашият ключ за профила е необходим при вход от нови приложения или мрежови четци. Може да бъде намерен в профила."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Устройството е регистрирано"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "За да получи достъп до хранилището, устройството трябва да бъде упълномощено от собственика на хранилището."; +"hubAuthentication.requireAccountInit.alert.title" = "Необходимо е действие"; +"hubAuthentication.requireAccountInit.alert.message" = "За да продължите завършете необходимите стъпки в потребителския профил в Hub."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Към профила"; +"purchase.retry.button" = "Повторен опит"; + +"settings.title" = "Настройки"; + +"s3Authentication.displayName" = "Показвано име"; +"s3Authentication.accessKey" = "Ключ за достъп"; +"s3Authentication.secretKey" = "Таен ключ"; +"s3Authentication.existingBucket" = "Bucket"; +"s3Authentication.endpoint" = "Крайна точка"; +"s3Authentication.region" = "Регион"; + +"unlockVault.button.unlock" = "Отключване"; + +"vaultDetail.button.changeVaultPassword" = "Промяна на парола"; +"vaultDetail.button.moveVault" = "Преместване"; +"vaultDetail.button.renameVault" = "Преименуване"; diff --git a/SharedResources/bn.lproj/Localizable.strings b/SharedResources/bn.lproj/Localizable.strings index cca673947..59ff54afb 100644 --- a/SharedResources/bn.lproj/Localizable.strings +++ b/SharedResources/bn.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "সক্রিয় করুন"; "common.button.next" = "পরবর্তী"; "common.button.ok" = "আচ্ছা"; +"common.button.refresh" = "রিফ্রেশ"; "common.button.remove" = "বাতিল"; "common.button.retry" = "পুনরায় চেষ্টা করুন"; "common.button.signOut" = "সাইন আউট"; diff --git a/SharedResources/ca.lproj/Localizable.strings b/SharedResources/ca.lproj/Localizable.strings index 4d581a11b..e1653121c 100644 --- a/SharedResources/ca.lproj/Localizable.strings +++ b/SharedResources/ca.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "Activar"; "common.button.next" = "Següent"; "common.button.ok" = "D'acord"; +"common.button.refresh" = "Refresca"; "common.button.remove" = "Elimina"; "common.button.retry" = "Reintenta"; "common.button.signOut" = "Tanca la sessió"; @@ -110,6 +111,11 @@ "getFolderIntent.error.missingPath" = "No s'ha proporcionat cap ruta. Proporcioneu una ruta vàlida que retorni una carpeta."; "getFolderIntent.error.noVaultSelected" = "No s'ha seleccionat cap caixa forta."; +"hubAuthentication.accessNotGranted" = "El vostre dispositiu no ha estat encara autoritzat a accedir a aquesta caixa forta. Demaneu autorització al propietari."; +"hubAuthentication.licenseExceeded" = "Aquest Cryptomator Hub no té una llicència vàlida. Informa si us plau a l'administrador perquè actualitzi o renovi la llicència."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nom del dispositiu"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Per a accedir a la caixa forta, el vostre dispositiu ha de ser autoritzat pel propietari de la caixa."; + "intents.saveFile.missingFile" = "El fitxer proporcionat no és vàlid."; "intents.saveFile.invalidFolder" = "La carpeta proporcionada no és vàlida."; "intents.saveFile.missingTemporaryFolder" = "No s'ha pogut crear la carpeta temporal."; diff --git a/SharedResources/cs.lproj/Localizable.strings b/SharedResources/cs.lproj/Localizable.strings index 8faca291c..896b5ac67 100644 --- a/SharedResources/cs.lproj/Localizable.strings +++ b/SharedResources/cs.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "Povolit"; "common.button.next" = "Další"; "common.button.ok" = "OK"; +"common.button.refresh" = "Načíst znovu"; "common.button.remove" = "Odstranit"; "common.button.retry" = "Opakovat"; "common.button.signOut" = "Odhlásit se"; @@ -101,15 +102,23 @@ "fileProvider.error.unlockButton" = "Odemknout"; "fileProvider.clearFileFromCache.title" = "Smazat soubor z mezipaměti"; "fileProvider.clearFileFromCache.message" = "Pouze smaže soubor z Vašeho zařízení a ponechá soubor v cloudu."; +"fileProvider.fileImporting.error.missingPremium" = "Odemkněte plnou verzi aplikace Cryptomator a získejte přístup k zápisu do vašeho trezoru."; "fileProvider.uploadProgress.connecting" = "Připojování…"; "fileProvider.uploadProgress.message" = "Aktuální průběh: %@\n\nPokud se domníváte, že proces nahrávání je zaseknutý, můžete zkusit nahrávání znovu."; "fileProvider.uploadProgress.missing" = "Průběh nebylo možné zjistit. Může nadále pokračovat na pozadí."; "fileProvider.uploadProgress.title" = "Nahrávání…"; "fileProvider.uploadProgress.missingDomainError" = "Doména nebyla nalezena."; + +"getFolderIntent.error.missingPath" = "Nebyla zadána žádná cesta. Uveďte prosím platnou cestu, pro kterou má být složka vrácena."; "getFolderIntent.error.noVaultSelected" = "Nebyl vybrán žádný trezor."; +"hubAuthentication.accessNotGranted" = "Vaše zařízení dosud nebylo oprávněno k přístupu k tomuto trezoru. Požádejte vlastníka trezoru, aby jej autorizoval."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Název zařízení"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Pro přístup k trezoru musí být vaše zařízení autorizováno majitelem trezoru."; + "intents.saveFile.missingFile" = "Zadaný soubor není platný."; "intents.saveFile.invalidFolder" = "Zadaná složka není platná."; "intents.saveFile.missingTemporaryFolder" = "Vytvoření dočasné složky se nezdařilo."; +"intents.saveFile.lockedVault" = "Musíte odemknout váš trezor, abyste mohli používat tuto zkratku."; "intents.saveFile.selectedVaultNotFound" = "Vybraný trezor nebyl nalezen."; "keepUnlocked.alert.title" = "Zamknout trezor?"; @@ -209,6 +218,7 @@ "s3Authentication.endpoint" = "Koncový bod"; "s3Authentication.region" = "Oblast"; "s3Authentication.error.invalidCredentials" = "Neplatné přihlašovací údaje."; +"s3Authentication.error.invalidEndpoint" = "Zadaný endpoint neodpovídá formátu URL."; "trialStatus.active" = "Aktivní"; "trialStatus.expired" = "Expirováno"; diff --git a/SharedResources/da.lproj/Localizable.strings b/SharedResources/da.lproj/Localizable.strings index b16a04c8d..8d789792e 100644 --- a/SharedResources/da.lproj/Localizable.strings +++ b/SharedResources/da.lproj/Localizable.strings @@ -15,18 +15,19 @@ "common.button.confirm" = "Bekræft"; "common.button.create" = "Opret"; "common.button.createFolder" = "Opret mappe"; -"common.button.done" = "Ferdig"; +"common.button.done" = "Færdig"; "common.button.download" = "Download"; "common.button.edit" = "Redigér"; "common.button.enable" = "Aktivér"; "common.button.next" = "Næste"; "common.button.ok" = "OK"; +"common.button.refresh" = "Opdatér"; "common.button.remove" = "Fjern"; "common.button.retry" = "Forsøg igen"; "common.button.signOut" = "Log ud"; "common.button.verify" = "Bekræft"; "common.cells.openInFilesApp" = "Åbn i Filer appen"; -"common.cells.password" = "Passord"; +"common.cells.password" = "Adgangskode"; "common.cells.url" = "URL"; "common.cells.username" = "Brugernavn"; "common.footer.learnMore" = "Læs mere."; @@ -37,13 +38,13 @@ "accountList.signOut.alert.title" = "Fjern associerede bokse?"; "accountList.signOut.alert.message" = "Ved at logge ud, vil alle tilknyttede bokse blive fjernet fra listen. Ingen krypterede data vil blive slettet. Du kan logge ind igen og tilføje boksene igen senere."; -"addVault.title" = "Legg til kvelv"; -"addVault.createNewVault.title" = "Opprett ein ny kvelv"; +"addVault.title" = "Tilføj boks"; +"addVault.createNewVault.title" = "Opret ny boks"; "addVault.createNewVault.purchase" = "Oprettelse af en ny boks kræver den fulde version af Cryptomator."; "addVault.createNewVault.setVaultName.header.title" = "Vælg et navn til boksen."; -"addVault.createNewVault.setVaultName.cells.name" = "Namn på kvelven"; +"addVault.createNewVault.setVaultName.cells.name" = "Boks-navn"; "addVault.createNewVault.setVaultName.error.emptyVaultName" = "Boks navn må ikke være tomt."; -"addVault.createNewVault.chooseCloud.header" = "Kvar skal Cryptomator lagra dei krypterte filene i kvelven din?"; +"addVault.createNewVault.chooseCloud.header" = "Hvor skal Cryptomator gemme de krypterede filer af din boks?"; "addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" findes allerede på dette sted. Vælg et andet navn eller en anden placering."; "addVault.createNewVault.detectedMasterkey.text" = "Cryptomator detekterede en eksisterende boks på denne placering.\nFor at oprette en ny boks, skal du gå tilbage og vælge en anden mappe."; "addVault.createNewVault.password.enterPassword.header" = "Indtast en ny adgangskode."; @@ -54,7 +55,7 @@ "addVault.createNewVault.password.error.nonMatchingPasswords" = "Adgangskoderne er ikke ens."; "addVault.createNewVault.password.error.tooShortPassword" = "Adgangskoden skal indeholde mindst 8 tegn."; "addVault.createNewVault.progress" = "Opretter boks…"; -"addVault.openExistingVault.title" = "Opn ein eksisterande kvelv"; +"addVault.openExistingVault.title" = "Open eksisterende boks"; "addVault.openExistingVault.chooseCloud.header" = "Hvor er boksen placeret?"; "addVault.openExistingVault.detectedMasterkey.text" = "Kryptomator fandt boksen \"%@\".\nVil du tilføje denne boks?"; "addVault.openExistingVault.detectedMasterkey.add" = "Tilføj denne boks"; @@ -110,6 +111,11 @@ "getFolderIntent.error.missingPath" = "Ingen sti blev angivet. Angiv venligst en gyldig sti til en mappe."; "getFolderIntent.error.noVaultSelected" = "Ingen boks er valgt."; +"hubAuthentication.accessNotGranted" = "Din enhed er endnu ikke blevet godkendt til at få adgang til denne boks. Spørg boks-ejeren om godkendelse."; +"hubAuthentication.licenseExceeded" = "Din Cryptomator Hub har en ugyldig licens. Få venligst en Hub administrator til at opgradere eller forny licensen."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Enheds-navn"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "For at tilgå boksen, skal din enhed godkendes af boks-ejeren."; + "intents.saveFile.missingFile" = "Den angivne fil er ikke gyldig."; "intents.saveFile.invalidFolder" = "Den angivne mappe er ikke gyldig."; "intents.saveFile.missingTemporaryFolder" = "Kunne ikke oprette midlertidig mappe."; @@ -247,7 +253,7 @@ "vaultAccountManager.error.vaultAccountAlreadyExists" = "Du har allerede tilføjet denne boks."; -"vaultDetail.button.changeVaultPassword" = "Byt passord"; +"vaultDetail.button.changeVaultPassword" = "Skift adgangskode"; "vaultDetail.button.lock" = "Lås nu"; "vaultDetail.button.moveVault" = "Flyt"; "vaultDetail.button.removeVault" = "Fjern fra listen over bokse"; diff --git a/SharedResources/de.lproj/Localizable.strings b/SharedResources/de.lproj/Localizable.strings index 8af61043c..221a89aec 100644 --- a/SharedResources/de.lproj/Localizable.strings +++ b/SharedResources/de.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Aktivieren"; "common.button.next" = "Weiter"; "common.button.ok" = "OK"; +"common.button.refresh" = "Aktualisieren"; +"common.button.register" = "Registrieren"; "common.button.remove" = "Entfernen"; "common.button.retry" = "Wiederholen"; "common.button.signOut" = "Ausloggen"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Wo befindet sich der Tresor?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator hat den Tresor „%@“ erkannt.\nMöchtest du diesen Tresor hinzufügen?"; "addVault.openExistingVault.detectedMasterkey.add" = "Diesen Tresor hinzufügen"; +"addVault.openExistingVault.downloadVault.progress" = "Tresor wird heruntergeladen…"; "addVault.openExistingVault.password.footer" = "Gib das Passwort für „%@“ ein."; "addVault.openExistingVault.progress" = "Tresor wird hinzugefügt …"; "addVault.success.info" = "Tresor „%@“ erfolgreich hinzugefügt.\nGreife auf diesen Tresor über die App „Dateien“ zu."; @@ -80,7 +83,7 @@ "cloudProvider.error.itemNotFound" = "„%@“ konnte nicht gefunden werden."; "cloudProvider.error.itemAlreadyExists" = "„%@“ existiert bereits."; -"cloudProvider.error.itemTypeMismatch" = "\"%@\" hat einen unerwarteten Elementtyp."; +"cloudProvider.error.itemTypeMismatch" = "„%@“ hat einen unerwarteten Dateityp."; "cloudProvider.error.parentFolderDoesNotExist" = "Übergeordneter Ordner „%@“ existiert nicht."; "cloudProvider.error.pageTokenInvalid" = "Abrufen von Verzeichnisinhalten konnte nicht fortgesetzt werden."; "cloudProvider.error.quotaInsufficient" = "Dein Speicher hat nicht genügend Platz."; @@ -108,9 +111,22 @@ "fileProvider.uploadProgress.title" = "Wird hochgeladen …"; "fileProvider.uploadProgress.missingDomainError" = "Domain konnte nicht gefunden werden."; -"getFolderIntent.error.missingPath" = "Es wurde kein Pfad angegeben. Bitte gib einen gültigen Pfad zu einem Ordner an."; +"getFolderIntent.error.missingPath" = "Es wurde kein Pfad angegeben. Bitte geben Sie einen gültigen Pfad an, für den ein Ordner angegeben werden soll."; "getFolderIntent.error.noVaultSelected" = "Es wurde kein Tresor ausgewählt."; -"intents.saveFile.missingFile" = "Die angegebene Datei ist ungültig."; + +"hubAuthentication.title" = "Hubtresor"; +"hubAuthentication.accessNotGranted" = "Dein Gerät wurde noch nicht für den Zugriff auf diesen Tresor autorisiert. Bitte den Tresorbesitzer, dein Gerät zu autorisieren."; +"hubAuthentication.licenseExceeded" = "Die Lizenz deiner Cryptomator-Hub-Instanz ist ungültig. Bitte informiere deinen Hub-Administrator, um die Lizenz zu erweitern oder zu erneuern."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Gerätename"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Dies scheint der erste Hub-Zugriff von diesem Gerät zu sein. Um es für die Zugriffsberechtigung zu identifizieren, musst du diesem Gerät einen Namen geben."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Ihr Account Key ist erforderlich, um sich von neuen Anwendungen oder Browsern aus anzumelden. Sie können ihn in Ihrem Profil finden."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Gerät erfolgreich registriert"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Für Zugriff auf den Tresor muss dein Gerät vom Tresorbesitzer autorisiert werden."; +"hubAuthentication.requireAccountInit.alert.title" = "Handlung erforderlich"; +"hubAuthentication.requireAccountInit.alert.message" = "Um fortzufahren, führen Sie bitte die erforderlichen Schritte in Ihrem Hub Benutzerprofil aus."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Zum Profil"; + +"intents.saveFile.missingFile" = "Die bereitgestellte Datei ist nicht gültig."; "intents.saveFile.invalidFolder" = "Der angegebene Ordner ist ungültig."; "intents.saveFile.missingTemporaryFolder" = "Erstellung eines temporären Ordners fehlgeschlagen."; "intents.saveFile.lockedVault" = "Du musst deinen Tresor entsperren, um diesen Kurzbefehl nutzen zu können."; diff --git a/SharedResources/el.lproj/Localizable.strings b/SharedResources/el.lproj/Localizable.strings index 69900ae7d..2c1eeef59 100644 --- a/SharedResources/el.lproj/Localizable.strings +++ b/SharedResources/el.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Ενεργοποίηση"; "common.button.next" = "Επόμενο"; "common.button.ok" = "ΟΚ"; +"common.button.refresh" = "Ανανέωση"; +"common.button.register" = "Εγγραφή"; "common.button.remove" = "Αφαίρεση"; "common.button.retry" = "Επανάληψη"; "common.button.signOut" = "Αποσύνδεση"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Πού βρίσκεται η κρυπτή;"; "addVault.openExistingVault.detectedMasterkey.text" = "Το Cryptomator εντόπισε την κρύπτη \"%@\".\nΘα θέλατε να προσθέσετε αυτή την κρύπτη;"; "addVault.openExistingVault.detectedMasterkey.add" = "Προσθήκη αυτής της κρύπτης"; +"addVault.openExistingVault.downloadVault.progress" = "Λήψη Κρύπτης…"; "addVault.openExistingVault.password.footer" = "Εισάγετε τον κωδικό για \"%@\"."; "addVault.openExistingVault.progress" = "Προσθήκη Κρύπτης…"; "addVault.success.info" = "Προστέθηκε με επιτυχία η κρύπτη \"%@\".\nΠρόσβαση σε αυτή την κρύπτη μέσω της εφαρμογής Αρχείων."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Δε δόθηκε καμία διαδρομή. Παρακαλώ δώστε μια έγκυρη διαδρομή για την οποία θα πρέπει να επιστραφεί ένας φάκελος."; "getFolderIntent.error.noVaultSelected" = "Δεν έχει επιλεγεί κρύπτη."; + +"hubAuthentication.title" = "Κρύπτη Hub"; +"hubAuthentication.accessNotGranted" = "Η συσκευή σας δεν έχει ακόμη εξουσιοδοτηθεί να έχει πρόσβαση σε αυτή την κρύπτη. Ζητήστε από τον κάτοχο της κρύπτης να την εξουσιοδοτήσει."; +"hubAuthentication.licenseExceeded" = "Η συνεδρία σας στο Cryptomator Hub έχει μη έγκυρη άδεια χρήσης. Ενημερώστε έναν διαχειριστή του Hub για να αναβαθμίσει ή να ανανεώσει την άδεια χρήσης."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Όνομα Συσκευής"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Αυτή φαίνεται να είναι η πρώτη πρόσβαση στο Hub από αυτήν τη συσκευή. Για να την αναγνωρίσετε για εξουσιοδότηση πρόσβασης, πρέπει να ονομάσετε αυτήν τη συσκευή."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Το Κλειδί Λογαριασμού σας απαιτείται για να συνδεθείτε από νέες εφαρμογές ή προγράμματα περιήγησης. Μπορείτε να το βρείτε στο προφίλ σας."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Επιτυχής Εγγραφή Συσκευής"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Για να αποκτήσετε πρόσβαση στην κρύπτη, η συσκευή σας πρέπει να είναι εξουσιοδοτημένη από τον κάτοχο της κρύπτης."; +"hubAuthentication.requireAccountInit.alert.title" = "Απαιτείται Ενέργεια"; +"hubAuthentication.requireAccountInit.alert.message" = "Για να συνεχίσετε, παρακαλούμε συμπληρώστε τα βήματα που απαιτούνται στο προφίλ χρήστη Hub σας."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Πηγαίνετε στο Προφίλ"; + "intents.saveFile.missingFile" = "Το παρεχόμενο αρχείο δεν είναι έγκυρο."; "intents.saveFile.invalidFolder" = "Ο παρεχόμενος φάκελος δεν είναι έγκυρος."; "intents.saveFile.missingTemporaryFolder" = "Αποτυχία δημιουργίας προσωρινού φακέλου."; diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 69293b1d4..5a1eb77f1 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Enable"; "common.button.next" = "Next"; "common.button.ok" = "OK"; +"common.button.refresh" = "Refresh"; +"common.button.register" = "Register"; "common.button.remove" = "Remove"; "common.button.retry" = "Retry"; "common.button.signOut" = "Sign Out"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Where is the vault located?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator detected the vault \"%@\".\nWould you like to add this vault?"; "addVault.openExistingVault.detectedMasterkey.add" = "Add This Vault"; +"addVault.openExistingVault.downloadVault.progress" = "Downloading Vault…"; "addVault.openExistingVault.password.footer" = "Enter password for \"%@\"."; "addVault.openExistingVault.progress" = "Adding Vault…"; "addVault.success.info" = "Successfully added vault \"%@\".\nAccess this vault via the Files app."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "No path was provided. Please provide a valid path for which a folder should be returned."; "getFolderIntent.error.noVaultSelected" = "No vault has been selected."; + +"hubAuthentication.title" = "Hub Vault"; +"hubAuthentication.accessNotGranted" = "Your device has not yet been authorized to access this vault. Ask the vault owner to authorize it."; +"hubAuthentication.licenseExceeded" = "Your Cryptomator Hub instance has an invalid license. Please inform a Hub administrator to upgrade or renew the license."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Device Name"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Your Account Key is required to login from new apps or browsers. It can be found in your profile."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Register Device Successful"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "To access the vault, your device needs to be authorized by the vault owner."; +"hubAuthentication.requireAccountInit.alert.title" = "Action Required"; +"hubAuthentication.requireAccountInit.alert.message" = "To proceed, please complete the steps required in your Hub user profile."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Go to Profile"; + "intents.saveFile.missingFile" = "The provided file is not valid."; "intents.saveFile.invalidFolder" = "The provided folder is not valid."; "intents.saveFile.missingTemporaryFolder" = "Failed to create temporary folder."; diff --git a/SharedResources/es.lproj/Localizable.strings b/SharedResources/es.lproj/Localizable.strings index 95f5e754b..d94a1104a 100644 --- a/SharedResources/es.lproj/Localizable.strings +++ b/SharedResources/es.lproj/Localizable.strings @@ -19,8 +19,10 @@ "common.button.download" = "Descargar"; "common.button.edit" = "Editar"; "common.button.enable" = "Activar"; -"common.button.next" = "Continuar"; +"common.button.next" = "Siguiente"; "common.button.ok" = "Aceptar"; +"common.button.refresh" = "Recargar"; +"common.button.register" = "Registrarse"; "common.button.remove" = "Eliminar"; "common.button.retry" = "Reintentar"; "common.button.signOut" = "Cerrar sesión"; @@ -40,14 +42,14 @@ "addVault.title" = "Añadir bóveda"; "addVault.createNewVault.title" = "Crear bóveda nueva"; "addVault.createNewVault.purchase" = "Crear una bóveda nueva requiere la versión completa de Cryptomator."; -"addVault.createNewVault.setVaultName.header.title" = "Elegir un nombre para la bóveda."; +"addVault.createNewVault.setVaultName.header.title" = "Elija un nombre para la bóveda."; "addVault.createNewVault.setVaultName.cells.name" = "Nombre de la bóveda"; "addVault.createNewVault.setVaultName.error.emptyVaultName" = "El nombre de la bóveda no puede estar vacío."; "addVault.createNewVault.chooseCloud.header" = "¿Dónde se deben almacenar los archivos cifrados de la bóveda?"; "addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" ya existe en esta ubicación. Elija un nombre o ubicación diferente."; "addVault.createNewVault.detectedMasterkey.text" = "Cryptomator detectó una bóveda existente en esta ubicación.\nPara crear una bóveda nueva, por favor vuelva atrás y elija una carpeta diferente."; "addVault.createNewVault.password.enterPassword.header" = "Ingrese una contraseña nueva."; -"addVault.createNewVault.password.confirmPassword.header" = "Confirmar la contraseña nueva."; +"addVault.createNewVault.password.confirmPassword.header" = "Confirme la contraseña nueva."; "addVault.createNewVault.password.confirmPassword.alert.title" = "¿Confirmar contraseña?"; "addVault.createNewVault.password.confirmPassword.alert.message" = "IMPORTANTE: si olvida su contraseña no habrá manera de recuperar los datos."; "addVault.createNewVault.password.error.emptyPassword" = "La contraseña no puede estar vacía."; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "¿Dónde se ubica la bóveda?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator detectó la bóveda \"%@\".\n¿Desea añadir esta bóveda?"; "addVault.openExistingVault.detectedMasterkey.add" = "Añadir esta bóveda"; +"addVault.openExistingVault.downloadVault.progress" = "Descargando bóveda…"; "addVault.openExistingVault.password.footer" = "Ingresar contraseña para \"%@\"."; "addVault.openExistingVault.progress" = "Añadiendo bóveda…"; "addVault.success.info" = "Se ha añadido correctamente la bóveda \"%@\".\nAcceda a esta bóveda desde la aplicación de archivos."; @@ -69,7 +72,7 @@ "changePassword.error.invalidOldPassword" = "La contraseña actual es incorrecta. Intente de nuevo."; "changePassword.header.currentPassword.title" = "Ingrese la contraseña actual."; "changePassword.header.newPassword.title" = "Ingrese una contraseña nueva."; -"changePassword.header.newPasswordConfirmation.title" = "Confirmar la contraseña nueva."; +"changePassword.header.newPasswordConfirmation.title" = "Confirme la contraseña nueva."; "changePassword.progress" = "Cambiando contraseña…"; "chooseFolder.emptyFolder.footer" = "La carpeta está vacía"; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "No se ha proporcionado ninguna ruta. Proporcione una ruta válida para la que se debe devolver una carpeta."; "getFolderIntent.error.noVaultSelected" = "No se ha seleccionado una bóveda."; + +"hubAuthentication.title" = "Bóveda de Hub"; +"hubAuthentication.accessNotGranted" = "Su dispositivo aún no ha sido autorizado para acceder a esta bóveda. Pídale al propietario de la bóveda que lo autorice."; +"hubAuthentication.licenseExceeded" = "Su instancia del Hub de Cryptomator tiene una licencia inválida. Informe a un administrador del Hub para actualizar o renovar la licencia."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nombre del dispositivo"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Este parece ser el primer acceso al Hub desde este dispositivo. Para identificarlo y autorizar el acceso, necesita nombrar este dispositivo."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Se requiere su clave de cuenta para iniciar sesión desde nuevas aplicaciones o navegadores. Puede encontrarse en su perfil."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Registro del dispositivo exitoso"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Para acceder a la bóveda, su dispositivo debe ser autorizado por el propietario de la bóveda."; +"hubAuthentication.requireAccountInit.alert.title" = "Acción requerida"; +"hubAuthentication.requireAccountInit.alert.message" = "Para continuar, complete los pasos necesarios en su perfil de usuario de Hub."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Ir al Perfil"; + "intents.saveFile.missingFile" = "El archivo proporcionado es inválido."; "intents.saveFile.invalidFolder" = "La carpeta proporcionada es inválida."; "intents.saveFile.missingTemporaryFolder" = "Error al crear la carpeta temporal."; @@ -209,8 +225,8 @@ "s3Authentication.displayName" = "Nombre para mostrar"; "s3Authentication.accessKey" = "Clave de acceso"; "s3Authentication.secretKey" = "Clave secreta"; -"s3Authentication.existingBucket" = "Cubeta existente"; -"s3Authentication.endpoint" = "Punto final"; +"s3Authentication.existingBucket" = "Bucket existente"; +"s3Authentication.endpoint" = "Punto de enlace"; "s3Authentication.region" = "Región"; "s3Authentication.error.invalidCredentials" = "Credenciales inválidas."; "s3Authentication.error.invalidEndpoint" = "El punto final proporcionado no coincide con el formato de una URL."; diff --git a/SharedResources/fa.lproj/Localizable.strings b/SharedResources/fa.lproj/Localizable.strings index 81009d812..71b04e241 100644 --- a/SharedResources/fa.lproj/Localizable.strings +++ b/SharedResources/fa.lproj/Localizable.strings @@ -6,6 +6,7 @@ "common.button.done" = "انجام شده"; "common.button.edit" = "ویرایش"; "common.button.next" = "بعدی"; +"common.button.refresh" = "نوسازی"; "common.button.remove" = "حذف"; "common.button.retry" = "تلاش مجدد"; "common.cells.url" = "آدرس اینترنتی"; @@ -20,8 +21,12 @@ "purchase.retry.button" = "تلاش مجدد"; "settings.title" = "تنظیمات"; + +"s3Authentication.displayName" = "نام"; "s3Authentication.accessKey" = "کلید دسترسی"; "s3Authentication.secretKey" = "کلید مخفی"; +"s3Authentication.existingBucket" = "صندوقچه فعلی"; +"s3Authentication.endpoint" = "نقطه انتهایی"; "s3Authentication.region" = "منطقه"; "unlockVault.button.unlock" = "بازکردن قفل"; diff --git a/SharedResources/fi.lproj/Localizable.strings b/SharedResources/fi.lproj/Localizable.strings new file mode 100644 index 000000000..b483434b9 --- /dev/null +++ b/SharedResources/fi.lproj/Localizable.strings @@ -0,0 +1,20 @@ +"common.button.cancel" = "Peruuta"; +"common.button.change" = "Muuta"; +"common.button.close" = "Sulje"; +"common.button.done" = "Valmis"; +"common.button.next" = "Seuraava"; + +"addVault.title" = "Lisää Vault"; +"addVault.createNewVault.title" = "Luo Uusi Vault"; +"addVault.createNewVault.setVaultName.cells.name" = "Vault Nimi"; +"addVault.createNewVault.chooseCloud.header" = "Missä pitäisi Cryptomator tallentaa salattuja tiedostoja Vault?"; +"addVault.openExistingVault.title" = "Avaa Olemassaoleva Vault"; +"fileProvider.error.unlockButton" = "Avaa"; +"hubAuthentication.accessNotGranted" = "Laitteellasi ei ole pääsyvaltuutusta tähän holviin. Pyydä holvin omistajaa lisäämän valtuutus laitteellesi."; +"hubAuthentication.licenseExceeded" = "Cryptomator Hub:illasi ei ole voimassa olevaa lisenssiä. Ole hyvä ja ilmoita Hubin järjestelmänvalvojalle lisenssin päivittämiseksi tai sen uusimiseksi."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Laitteen Nimi"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Käyttääksesi holvia, holvin omistajan on valtuutettava laitteesi."; + +"unlockVault.button.unlock" = "Avaa"; + +"vaultDetail.button.changeVaultPassword" = "Vaihda salasana"; diff --git a/SharedResources/fil.lproj/Localizable.strings b/SharedResources/fil.lproj/Localizable.strings index bafcdc5c3..98a7ecc96 100644 --- a/SharedResources/fil.lproj/Localizable.strings +++ b/SharedResources/fil.lproj/Localizable.strings @@ -1,37 +1,295 @@ +/* + Localizable.strings + Cryptomator + + Copyright © 2021 Skymatic GmbH. All rights reserved. +*/ + +"common.alert.error.title" = "Error"; "common.alert.attention.title" = "Atensyon"; "common.button.cancel" = "Kanselahin"; "common.button.change" = "Baguhin"; "common.button.choose" = "Pumili"; +"common.button.clear" = "Maaliwalas"; "common.button.close" = "Isara"; +"common.button.confirm" = "Kumpirmahin"; "common.button.create" = "Gumawa"; +"common.button.createFolder" = "Gumawa ng Folder"; "common.button.done" = "Tapos na"; +"common.button.download" = "I-download"; "common.button.edit" = "I-edit"; "common.button.enable" = "I-enable"; "common.button.next" = "Sunod"; "common.button.ok" = "OK"; +"common.button.refresh" = "I-refresh"; "common.button.remove" = "Tanggalin"; "common.button.retry" = "Subukan muli"; +"common.button.signOut" = "Mag-sign Out"; +"common.button.verify" = "I-verify"; +"common.cells.openInFilesApp" = "Ibuksan sa Files App"; +"common.cells.password" = "Password"; "common.cells.url" = "URL"; "common.cells.username" = "Username"; +"common.footer.learnMore" = "Matuto pa."; +"common.hud.authenticating" = "Pinapatunayan…"; + +"accountList.header.title" = "Mga pagpapatunay"; +"accountList.emptyList.message" = "Pumindot dito para madagdag ang account"; +"accountList.signOut.alert.title" = "Itangal ang mga associated na vaults?"; +"accountList.signOut.alert.message" = "Sa pamamagitan ng pag-sign out, ang lahat ng nauugnay na vault ay aalisin sa listahan ng vault. Walang matatanggal na naka-encrypt na data. Maaari kang mag-sign in muli at muling idagdag ang mga vault sa ibang pagkakataon."; "addVault.title" = "Magdagdag ng Vault"; "addVault.createNewVault.title" = "Gumawa ng Bagong Vault"; +"addVault.createNewVault.purchase" = "Ang paggawa ng bagong vault ay nangangailangan ng buong bersyon ng Cryptomator."; +"addVault.createNewVault.setVaultName.header.title" = "Pumili ng pangalan para sa vault."; "addVault.createNewVault.setVaultName.cells.name" = "Pangalan ng Vault"; +"addVault.createNewVault.setVaultName.error.emptyVaultName" = "Hindi pwedeng wala ang pangalan ng vault."; "addVault.createNewVault.chooseCloud.header" = "Saan maaaring ilagay ng Cryptomator ang mga encrypted files ng iyong vault?"; +"addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "Umiiral na ang \"%@\" sa lokasyong ito. Pumili ng ibang pangalan o lokasyon ng vault."; +"addVault.createNewVault.detectedMasterkey.text" = "Nakakita ang Cryptomator ng umiiral nang vault sa lokasyong ito.\nUpang gumawa ng bagong vault, mangyaring bumalik at pumili ng ibang folder."; +"addVault.createNewVault.password.enterPassword.header" = "Maglagay ng bagong password."; +"addVault.createNewVault.password.confirmPassword.header" = "Kumpirmahin ang bagong password."; +"addVault.createNewVault.password.confirmPassword.alert.title" = "Kumpirmahin ang Password?"; "addVault.createNewVault.password.confirmPassword.alert.message" = "IMPORTANTE: Walang paraan para maisalba ang iyong datos kapag nakalimutan ang iyong password."; +"addVault.createNewVault.password.error.emptyPassword" = "Hindi maaaring walang laman ang password."; +"addVault.createNewVault.password.error.nonMatchingPasswords" = "Hindi tugma ang mga password."; +"addVault.createNewVault.password.error.tooShortPassword" = "8 o higit pang karakter ang kailangan sa password."; +"addVault.createNewVault.progress" = "Gumagawa ng Vault…"; "addVault.openExistingVault.title" = "Magbukas ng Umiiral na Vault"; +"addVault.openExistingVault.chooseCloud.header" = "Saan matatagpuan ang vault?"; +"addVault.openExistingVault.detectedMasterkey.text" = "Natukoy ng Cryptomator ang vault na \"%@\".\nGusto mo bang idagdag ang vault na ito?"; +"addVault.openExistingVault.detectedMasterkey.add" = "Idagdag ang Vault na ito"; +"addVault.openExistingVault.password.footer" = "Ipasok ang password para sa \"%@\"."; +"addVault.openExistingVault.progress" = "Idinaragdag ang Vault…"; +"addVault.success.info" = "Matagumpay na naidagdag ang vault na \"%@\".\nI-access ang vault na ito sa pamamagitan ng Files app."; +"addVault.success.footer" = "Kung hindi mo pa nagagawa, paganahin ang Cryptomator sa Files app."; + +"biometryType.faceID" = "Face ID"; +"biometryType.touchID" = "Pindutin ang ID"; + +"changePassword.error.invalidOldPassword" = "Mali ang kasalukuyang password. Pakisubukang muli."; +"changePassword.header.currentPassword.title" = "Ipasok ang kasalukuyang password."; +"changePassword.header.newPassword.title" = "Maglagay ng bagong password."; +"changePassword.header.newPasswordConfirmation.title" = "Kumpirmahin ang bagong password."; +"changePassword.progress" = "Pinapalitan ang Password…"; + +"chooseFolder.emptyFolder.footer" = "Walang laman ang folder"; +"chooseFolder.createNewFolder.header.title" = "Pumili ng pangalan para sa folder."; +"chooseFolder.createNewFolder.cells.name" = "Pangalan ng Folder"; +"chooseFolder.createNewFolder.error.emptyFolderName" = "Hindi maaaring walang laman ang pangalan ng folder."; +"chooseFolder.createNewFolder.progress" = "Gumagawa ng Folder…"; + +"cloudProvider.error.itemNotFound" = "Hindi mahanap ang \"%@\"."; +"cloudProvider.error.itemAlreadyExists" = "\"%@\" mayroon na."; +"cloudProvider.error.itemTypeMismatch" = "Ang \"%@\" ay may hindi inaasahang uri ng item."; +"cloudProvider.error.parentFolderDoesNotExist" = "Ang folder ng magulang na \"%@\" ay hindi umiiral."; +"cloudProvider.error.pageTokenInvalid" = "Hindi maipagpatuloy ang pagkuha ng mga nilalaman ng direktoryo."; +"cloudProvider.error.quotaInsufficient" = "Walang sapat na espasyo ang iyong storage."; +"cloudProvider.error.unauthorized" = "Hindi magawa ang hindi awtorisadong operasyon."; +"cloudProvider.error.noInternetConnection" = "Kailangan ng koneksyon sa internet para sa operasyong ito."; + +"cloudProviderType.localFileSystem" = "Iba pang File Provider"; + +"fileProvider.onboarding.title" = "Maligayang pagdating"; +"fileProvider.onboarding.info" = "Salamat sa pagpili sa Cryptomator para protektahan ang iyong mga file. Upang makapagsimula, pumunta sa pangunahing app at magdagdag ng vault."; +"fileProvider.onboarding.button.openCryptomator" = "Buksan ang Cryptomator"; +"fileProvider.error.biometricalAuthCanceled.title" = "Nakansela ang Unlock"; +"fileProvider.error.biometricalAuthCanceled.message" = "Hindi matagumpay ang pag-unlock sa pamamagitan ng %@. Pakisubukang muli."; +"fileProvider.error.biometricalAuthWrongPassword.title" = "Mali ang Password"; +"fileProvider.error.biometricalAuthWrongPassword.message" = "Mali ang password na na-save para sa %@. Pakisubukang muli at ilagay ang iyong password upang muling paganahin ang %@."; +"fileProvider.error.defaultLock.title" = "Kinakailangan ang I-unlock"; +"fileProvider.error.defaultLock.message" = "Upang ma-access at maipakita ang mga nilalaman ng iyong vault, kailangan itong i-unlock."; "fileProvider.error.unlockButton" = "I-unlock"; +"fileProvider.clearFileFromCache.title" = "I-clear ang File mula sa Cache"; +"fileProvider.clearFileFromCache.message" = "Inaalis lang nito ang lokal na file sa iyong device at hindi tinatanggal ang file sa cloud."; +"fileProvider.fileImporting.error.missingPremium" = "I-unlock ang buong bersyon sa Cryptomator app para magkaroon ng access sa pagsulat sa iyong mga vault."; +"fileProvider.uploadProgress.connecting" = "Kumokonekta…"; +"fileProvider.uploadProgress.message" = "Kasalukuyang Pag-unlad: %@\n\nKung napapansin mong natigil ang pag-usad ng pag-upload, maaari mong subukang muli ang pag-upload."; +"fileProvider.uploadProgress.missing" = "Hindi matukoy ang pag-unlad. Maaaring tumatakbo pa rin ito sa background."; +"fileProvider.uploadProgress.title" = "Ina-upload…"; +"fileProvider.uploadProgress.missingDomainError" = "Hindi mahanap ang domain."; + +"getFolderIntent.error.missingPath" = "Walang ibinigay na landas. Mangyaring magbigay ng wastong landas kung saan dapat ibalik ang isang folder."; +"getFolderIntent.error.noVaultSelected" = "Walang vault na napili."; +"hubAuthentication.accessNotGranted" = "Hindi pa pinahihintulutan ang iyong device na i-access ang vault na ito. Hilingin sa may-ari ng vault na pahintulutan ito."; +"hubAuthentication.licenseExceeded" = "Ang iyong Cryptomator Hub instance ay may di-wastong lisensya. Mangyaring ipagbigay-alam sa administrator ng Hub na mag-upgrade o mag-renew ng lisensya."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Pangalan ng device"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Para ma-access ang vault, kailangang pahintulutan ng may-ari ng vault ang iyong device."; + +"intents.saveFile.missingFile" = "Ang ibinigay na file ay hindi wasto."; +"intents.saveFile.invalidFolder" = "Ang ibinigay na folder ay hindi wasto."; +"intents.saveFile.missingTemporaryFolder" = "Nabigong gumawa ng pansamantalang folder."; +"intents.saveFile.lockedVault" = "Kailangan mong i-unlock ang iyong vault para magamit ang shortcut na ito."; +"intents.saveFile.selectedVaultNotFound" = "Ang naselect na vault ay hindi makita."; + +"keepUnlocked.alert.title" = "Lock Vault?"; +"keepUnlocked.alert.message" = "Kinakailangan ng pagbabagong ito na i-lock ang iyong vault upang magkabisa."; +"keepUnlocked.alert.confirm" = "Kumpirmahin at I-lock Ngayon"; +"keepUnlocked.header" = "Tukuyin kung gaano katagal mo gustong manatiling naka-unlock ang vault na ito kapag idle."; +"keepUnlocked.footer.auto" = "Ang pagpapasya sa iOS ay nangangahulugan na ang Cryptomator ay maaaring wakasan anumang oras upang magbakante ng memorya, na awtomatikong nagla-lock sa vault."; +"keepUnlocked.footer.on" = "Gamit ang napiling opsyon, kailangang maimbak ang isang kopya ng iyong key sa iOS keychain, hangga't naka-unlock ang vault."; +"keepUnlockedDuration.auto" = "Hayaang Awtomatikong Magpasya ang iOS"; +"keepUnlockedDuration.auto.shortDisplayName" = "Auto"; +"keepUnlockedDuration.indefinite" = "Walang katiyakan"; + +"localFileSystemAuthentication.createNewVault.header" = "Sa susunod na screen, piliin ang lokasyon ng storage para sa iyong bagong vault."; +"localFileSystemAuthentication.createNewVault.button" = "Piliin ang Lokasyon ng Storage"; +"localFileSystemAuthentication.createNewVault.error.detectedExistingVault" = "Mayroon nang vault sa lokasyong ito. Pakisubukang muli gamit ang ibang lokasyon ng storage."; +"localFileSystemAuthentication.openExistingVault.header" = "Sa susunod na screen, piliin ang folder ng iyong kasalukuyang vault."; +"localFileSystemAuthentication.openExistingVault.button" = "Piliin ang Vault Folder"; +"localFileSystemAuthentication.openExistingVault.error.noVaultFound" = "Ang napiling folder ay hindi isang vault. Pakisubukang muli gamit ang ibang folder."; +"localFileSystemAuthentication.info.footer" = "Hindi sinusuportahan ng mga file provider na naka-gray out ang \"pagpili ng mga folder.\" Ito ay hindi isang limitasyon ng Cryptomator."; + +"maintenanceModeError.runningCloudTask" = "Hindi maisagawa ang operasyon dahil ang ibang mga pagpapatakbo sa background para sa vault na ito ay kailangang tapusin muna. Subukang muli mamaya."; + +"nameValidation.error.endsWithPeriod" = "Hindi ka maaaring gumamit ng pangalan na nagtatapos sa tuldok. Mangyaring pumili ng ibang pangalan."; +"nameValidation.error.endsWithSpace" = "Hindi ka maaaring gumamit ng pangalan na nagtatapos sa isang puwang. Mangyaring pumili ng ibang pangalan."; +"nameValidation.error.containsIllegalCharacter" = "Hindi ka maaaring gumamit ng pangalan na naglalaman ng \"%@\". Mangyaring pumili ng ibang pangalan."; + +"onboarding.title" = "Maligayang pagdating"; +"onboarding.info" = "Salamat sa pagpili sa Cryptomator para protektahan ang iyong mga file.\n\nSa Cryptomator, nasa iyong mga kamay ang susi sa iyong data. Ine-encrypt ng Cryptomator ang iyong data nang mabilis at madali.\n\nAng app na ito ay ganap na isinama sa Files app. Tiyaking paganahin ang Cryptomator sa Files app sa ibang pagkakataon upang ma-access ang iyong mga vault."; +"onboarding.button.continue" = "Magpatuloy"; + +"purchase.beginFreeTrial.alert.title" = "Na-unlock ang Pagsubok"; +"purchase.expiredTrial" = "Ang iyong pagsubok ay nag-expire na."; +"purchase.footer.privacyPolicy" = "Patakaran sa Privacy"; +"purchase.footer.termsOfUse" = "Mga Tuntunin ng Paggamit"; +"purchase.header.feature.familySharing" = "Pagbabahaginan ng pamilya"; +"purchase.header.feature.openSource" = "Pag-unlad ng open-source"; +"purchase.header.feature.writeAccess" = "Magsulat ng access sa iyong mga vault"; +"purchase.product.donateAndUpgrade" = "Mag-donate at Mag-upgrade"; +"purchase.product.freeUpgrade" = "Libreng Upgrade"; +"purchase.product.lifetimeLicense" = "Panghabambuhay na Lisensya"; +"purchase.product.lifetimeLicense.duration" = "isang beses"; +"purchase.product.pricing.free" = "Libre"; +"purchase.product.trial" = "30-Araw na Pagsubok"; +"purchase.product.trial.expirationDate" = "Petsa ng pagkawalang bisa: %@"; +"purchase.product.trial.duration" = "sa loob ng 30 araw"; +"purchase.product.yearlySubscription" = "Taunang Subskripsyon"; +"purchase.product.yearlySubscription.duration" = "taun-taon"; +"purchase.readOnlyMode.alert.title" = "Read-Only Mode"; +"purchase.readOnlyMode.alert.message" = "Maaari mong i-unlock ang buong bersyon ng Cryptomator sa ibang pagkakataon sa mga setting at gamitin ito sa read-only na mode sa ngayon."; +"purchase.restorePurchase.button" = "Ibalik ang Pagbili"; +"purchase.restorePurchase.validTrialFound.alert.title" = "Ipinagpatuloy ang Pagsubok"; +"purchase.restorePurchase.validTrialFound.alert.message" = "Magagamit mo na ngayon ang buong bersyon ng Cryptomator sa limitadong panahon. Mag-e-expire ang iyong trial sa %@. Pagkatapos nito, maa-access pa rin ang iyong mga vault sa read-only mode."; +"purchase.restorePurchase.fullVersionFound.alert.title" = "Ibalik ang Matagumpay"; +"purchase.restorePurchase.fullVersionNotFound.alert.title" = "Walang Buong Bersyon"; +"purchase.restorePurchase.fullVersionNotFound.alert.message" = "Hindi namin mahanap ang isang dating binili na buong bersyon na maaaring ibalik. Mangyaring subukan ang isa pang pagpipilian."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Kwalipikado para sa Pag-upgrade"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Mukhang sinusubukan mong mag-upgrade mula sa isang mas lumang bersyon ng Cryptomator. Kung ganoon, mangyaring piliin ang opsyon na \"I-upgrade ang Alok\" sa halip."; "purchase.retry.button" = "Subukan muli"; +"purchase.retry.footer" = "Hindi ma-load ang mga available na produkto."; +"purchase.title" = "I-unlock ang Buong Bersyon"; +"purchase.unlockedFullVersion.message" = "Magagamit mo na ngayon ang buong bersyon ng Cryptomator. Maligayang pag-crypting!"; +"purchase.unlockedFullVersion.title" = "Salamat"; +"purchase.error.unknown" = "Hindi available ang pagbiling ito sa App Store sa hindi malamang dahilan. Pakisubukang muli sa ibang pagkakataon.\n\nKung magpapatuloy ang error na ito, subukang i-restart ang iyong device o mag-sign out at bumalik sa iyong Apple ID sa mga setting ng iOS."; "settings.title" = "Settings"; +"settings.aboutCryptomator" = "Tungkol sa Cryptomator"; +"settings.aboutCryptomator.title" = "Bersyon %@ (%@)"; +"settings.cacheSize" = "Laki ng Cache"; "settings.clearCache" = "Linisin ang Cache"; +"settings.cloudServices" = "Mga Serbisyo sa Cloud"; +"settings.contact" = "Makipag-ugnayan"; +"settings.debugMode" = "Debug Mode"; +"settings.debugMode.alert.message" = "Sa mode na ito, maaaring isulat ang sensitibong data sa isang log file sa iyong device (hal., mga filename at path). Ang mga password, cookies, atbp. ay tahasang hindi kasama.\n\nTandaang i-disable ang debug mode sa lalong madaling panahon."; +"settings.manageSubscriptions" = "Pamahalaan ang Subscription"; +"settings.rateApp" = "I-rate ang App"; +"settings.sendLogFile" = "Magpadala ng Log File"; +"settings.shortcutsGuide" = "Gabay sa Mga Shortcut"; +"settings.unlockFullVersion" = "I-unlock ang Buong Bersyon"; + +"snapshots.fileprovider.file1" = "/Accounting.numbers"; +"snapshots.fileprovider.file2" = "/Pangwakas na Presentasyon.key"; +"snapshots.fileprovider.file3" = "/Product Trailer.mov"; +"snapshots.fileprovider.file4" = "/Proposal.docx"; +"snapshots.fileprovider.file5" = "/ulat.pdf"; +"snapshots.fileprovider.folder3" = "/Lihim na Proyekto"; +"snapshots.fileprovider.folder2" = "/Lihim na Proyekto"; +"snapshots.fileprovider.folder1" = "/Mga Sertipiko"; +"snapshots.main.vault1" = "/Trabaho"; +"snapshots.main.vault2" = "/Pamilya"; +"snapshots.main.vault3" = "/Mga dokumento"; +"snapshots.main.vault4" = "/Paglalakbay sa California"; + +"s3Authentication.displayName" = "Display Name"; "s3Authentication.accessKey" = "Access Key"; +"s3Authentication.secretKey" = "Lihim na Susi"; +"s3Authentication.existingBucket" = "Umiiral na Balde"; +"s3Authentication.endpoint" = "Endpoint"; "s3Authentication.region" = "Rehiyon"; +"s3Authentication.error.invalidCredentials" = "Di-wastong mga kredensyal."; +"s3Authentication.error.invalidEndpoint" = "Ang ibinigay na endpoint ay hindi tumutugma sa format ng isang URL."; + +"trialStatus.active" = "Aktibo"; +"trialStatus.expired" = "Nag-expire na"; "unlockVault.button.unlock" = "I-unlock"; +"unlockVault.button.unlockVia" = "I-unlock sa pamamagitan ng %@"; +"unlockVault.password.footer" = "Ipasok ang password para sa \"%@\"."; +"unlockVault.enableBiometricalUnlock.switch" = "Paganahin ang %@"; +"unlockVault.enableBiometricalUnlock.footer" = "Sa halip na i-unlock ang iyong vault gamit ang iyong password, maaari mo itong i-unlock sa pamamagitan ng %@."; +"unlockVault.evaluatePolicy.reason" = "I-unlock ang iyong vault"; +"unlockVault.progress" = "Ina-unlock…"; + +"untrustedTLSCertificate.title" = "Di-wastong TLS Certificate"; +"untrustedTLSCertificate.message" = "Ang TLS Certificate ng \"%@\" ay hindi wasto. Gusto mo pa rin bang magtiwala dito?\n\n SHA-256: %@"; +"untrustedTLSCertificate.add" = "Magtiwala"; +"untrustedTLSCertificate.dismiss" = "Huwag Magtiwala"; + +"upgrade.title" = "I-upgrade ang Alok"; +"upgrade.notEligible.alert.title" = "Nabigo ang pag-upgrade"; +"upgrade.notEligible.alert.message" = "Hindi matukoy ng Cryptomator ang isang mas lumang bersyon na naka-install sa iyong device. Kung binili mo ito, mangyaring muling i-download ito mula sa App Store at subukang muli."; +"upgrade.info" = "Salamat sa pagtitiwala sa Cryptomator mula noong unang bersyon. Bilang isang tapat na user, karapat-dapat ka para sa isang libreng pag-upgrade."; + +"urlSession.error.httpError.401" = "Maling username at/o password."; +"urlSession.error.httpError.403" = "Hindi sapat na mga karapatan sa hiniling na mapagkukunan."; +"urlSession.error.httpError.404" = "Hindi nakita ang hiniling na mapagkukunan."; +"urlSession.error.httpError.405" = "Ang paraan ng kahilingan ay hindi sinusuportahan ng target na mapagkukunan."; +"urlSession.error.httpError.409" = "Humiling ng salungat sa kasalukuyang estado ng target na mapagkukunan."; +"urlSession.error.httpError.412" = "Tinanggihan ang pag-access sa target na mapagkukunan."; +"urlSession.error.httpError.default" = "Nabigo ang koneksyon sa network gamit ang status code na %ld."; +"urlSession.error.unexpectedResponse" = "Nagkaroon ng hindi inaasahang tugon sa network."; + +"vaultAccountManager.error.vaultAccountAlreadyExists" = "Naidagdag mo na ang vault na ito."; + +"vaultDetail.button.changeVaultPassword" = "Palitan ANG password"; +"vaultDetail.button.lock" = "I-lock ngayon"; "vaultDetail.button.moveVault" = "Ilipat"; +"vaultDetail.button.removeVault" = "Alisin sa Listahan ng Vault"; "vaultDetail.button.renameVault" = "Baguhin ang pangalan"; +"vaultDetail.changePassword.footer" = "Pumili ng malakas na password para sa iyong vault na ikaw lang ang nakakaalam at itago ito sa isang ligtas na lugar."; +"vaultDetail.disabledBiometricalUnlock.footer" = "Kung pinagana mo ang %@, maiimbak ang iyong password sa vault sa iOS keychain."; +"vaultDetail.enabledBiometricalUnlock.footer" = "Kakailanganin lang ang iyong password sa vault kung nabigo ang %@ authentication."; +"vaultDetail.info.footer.accessVault" = "I-access ang vault sa pamamagitan ng Files app."; +"vaultDetail.info.footer.accountInfo" = "Naka-log in bilang %@ sa pamamagitan ng %@."; +"vaultDetail.keepUnlocked.title" = "I-unlock ang Tagal"; +"vaultDetail.keepUnlocked.footer.off" = "Kakailanganin ang pag-unlock kapag ang Cryptomator ay winakasan ng Files app."; +"vaultDetail.keepUnlocked.footer.limitedDuration" = "Kakailanganin ang pag-unlock kapag ang iyong vault ay naging idle para sa %@."; +"vaultDetail.keepUnlocked.footer.unlimitedDuration" = "Walang pag-unlock ang kakailanganin maliban kung manu-manong naka-lock."; +"vaultDetail.locked.footer" = "Kasalukuyang naka-lock ang iyong vault."; +"vaultDetail.moveVault.detectedMasterkey.text" = "Nakakita ang Cryptomator ng umiiral nang vault sa lokasyong ito.\nUpang ilipat ang iyong vault, mangyaring bumalik at pumili ng ibang folder."; +"vaultDetail.moveVault.progress" = "Gumagalaw…"; +"vaultDetail.removeVault.footer" = "Aalisin lang nito ang vault sa listahan ng vault at hindi tatanggalin ang anumang naka-encrypt na file."; +"vaultDetail.renameVault.progress" = "Pinapalitan ang pangalan…"; +"vaultDetail.unlocked.footer" = "Kasalukuyang naka-unlock ang iyong vault sa Files app."; +"vaultDetail.unlockVault.footer" = "Ilagay ang password para sa \"%@\" para iimbak ito sa iOS keychain at para paganahin ang %@."; + +"vaultList.header.title" = "Mga Vault"; +"vaultList.emptyList.message" = "Mag-tap dito para magdagdag ng vault"; +"vaultList.remove.alert.title" = "Alisin ang Vault?"; +"vaultList.remove.alert.message" = "Aalisin lang nito ang vault sa listahan ng vault. Walang matatanggal na naka-encrypt na data. Maaari mong muling idagdag ang vault sa ibang pagkakataon."; + +"vaultProviderFactory.error.unsupportedVaultConfig" = "Hindi sinusuportahan ang configuration ng Vault. Pakitiyak na pinapatakbo mo ang pinakabagong bersyon ng Cryptomator."; +"vaultProviderFactory.error.unsupportedVaultVersion" = "Hindi sinusuportahan ang bersyon ng Vault na %ld. Ang vault na ito ay ginawa gamit ang mas luma o mas bagong bersyon ng Cryptomator."; "webDAVAuthentication.httpConnection.alert.title" = "Gamiting ang HTTPS?"; "webDAVAuthentication.httpConnection.alert.message" = "Ang pag-gamit ng HTTP ay hindi ligtas. Kung alam mo ang mga panganib, maaari kang tumuloy gamit ang HTTP."; "webDAVAuthentication.httpConnection.change" = "Gawing HTTPS"; +"webDAVAuthentication.httpConnection.continue" = "Panatilihin ang HTTP"; + +"webDAVAuthenticator.error.unsupportedProtocol" = "Mukhang hindi compatible sa WebDAV ang server. Pakisuri kung ginamit mo ang tamang URL."; +"webDAVAuthenticator.error.untrustedCertificate" = "Ang sertipiko ng server na ito ay hindi pinagkakatiwalaan. Maaaring kailanganin mong muling idagdag ang koneksyon sa WebDAV na ito."; + +"Retry Upload" = "Subukan muli ang Pag-upload"; +"Clear from Cache" = "I-clear mula sa Cache"; diff --git a/SharedResources/fr.lproj/Localizable.strings b/SharedResources/fr.lproj/Localizable.strings index dfd834b8b..c7bbe5b3c 100644 --- a/SharedResources/fr.lproj/Localizable.strings +++ b/SharedResources/fr.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Activer"; "common.button.next" = "Suivant"; "common.button.ok" = "OK"; +"common.button.refresh" = "Actualiser"; +"common.button.register" = "Inscription"; "common.button.remove" = "Retirer"; "common.button.retry" = "Réessayer"; "common.button.signOut" = "Se déconnecter"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Où se situe le coffre ?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator a détecté le coffre \"%@\".\nVoulez-vous ajouter ce coffre ?"; "addVault.openExistingVault.detectedMasterkey.add" = "Ajouter ce coffre"; +"addVault.openExistingVault.downloadVault.progress" = "Téléchargement du coffre…"; "addVault.openExistingVault.password.footer" = "Entrez le mot de passe pour “%@”."; "addVault.openExistingVault.progress" = "Ajout du Coffre-fort…"; "addVault.success.info" = "Le coffre \"%@\" a été ajouté avec succès.\nAccédez à ce coffre via l'application Fichiers."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Aucun chemin n'a été fourni. Veuillez fournir un chemin d'accès valide pour lequel un dossier doit être renvoyé."; "getFolderIntent.error.noVaultSelected" = "Aucun coffre n'a été sélectionné."; + +"hubAuthentication.title" = "Coffre dans Hub"; +"hubAuthentication.accessNotGranted" = "Votre appareil n'a pas encore été autorisé à accéder à ce coffre-fort. Demandez au propriétaire du coffre-fort de l'autoriser."; +"hubAuthentication.licenseExceeded" = "Votre instance Cryptomator Hub a une licence invalide. Veuillez informer un administrateur Hub pour la mettre à niveau ou la renouveler."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nom de l'appareil"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Il semble que ce soit le premier accès au Hub à partir de cet appareil. Afin de l'identifier pour l'autorisation d'accès, vous devez nommer cet appareil."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Votre clé de compte est requise pour vous connecter depuis de nouvelles applications ou de nouveaux navigateurs. Elle se trouve dans votre profil."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Enregistrement de l'appareil réussi"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Pour accéder au coffre-fort, votre appareil doit être autorisé par le propriétaire du coffre-fort."; +"hubAuthentication.requireAccountInit.alert.title" = "Action requise"; +"hubAuthentication.requireAccountInit.alert.message" = "Pour continuer, veuillez compléter les étapes requises dans votre profil d'utilisateur Hub."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Aller au profil"; + "intents.saveFile.missingFile" = "Le fichier fourni n'est pas valide."; "intents.saveFile.invalidFolder" = "Le dossier fourni n'est pas valide."; "intents.saveFile.missingTemporaryFolder" = "Impossible de créer un dossier temporaire."; diff --git a/SharedResources/gl.lproj/Localizable.strings b/SharedResources/gl.lproj/Localizable.strings index ffc086db2..ad4546046 100644 --- a/SharedResources/gl.lproj/Localizable.strings +++ b/SharedResources/gl.lproj/Localizable.strings @@ -6,6 +6,7 @@ "common.button.done" = "Feito"; "common.button.edit" = "Editar"; "common.button.next" = "Seguinte"; +"common.button.refresh" = "Actualizar"; "common.button.remove" = "Eliminar"; "common.button.retry" = "Tentar de novo"; "common.cells.url" = "URL"; diff --git a/SharedResources/he.lproj/Localizable.strings b/SharedResources/he.lproj/Localizable.strings index f131f6aea..2528c08cf 100644 --- a/SharedResources/he.lproj/Localizable.strings +++ b/SharedResources/he.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "הפעל"; "common.button.next" = "הבא"; "common.button.ok" = "אישור"; +"common.button.refresh" = "רענן"; "common.button.remove" = "הסר"; "common.button.retry" = "לנסות שוב"; "common.button.signOut" = "התנתק"; @@ -110,6 +111,11 @@ "getFolderIntent.error.missingPath" = "לא סופק נתיב. נא ספק נתיב תקין שממנו יש להחזיר תיקיה."; "getFolderIntent.error.noVaultSelected" = "לא נבחר קובץ vault."; +"hubAuthentication.accessNotGranted" = "המכשיר שלך טרם אושר לגשת לכספת הזאת. יש לבקש אישור גישה מבעל הכספת."; +"hubAuthentication.licenseExceeded" = "הרישיון שמותקן במופע ה- Cryptomator האב שלך אינו תקף. אנא ידע את מנהל ההאב שלך לשדרג או לחדש את הרישיון."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "שם מכשיר"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "כדי לגשת לכספת, המכשיר שלך צריך לקבל הרשאה על ידי בעלי הכספת."; + "intents.saveFile.missingFile" = "הקובץ שסופק אינו תקין."; "intents.saveFile.invalidFolder" = "התיקייה שנבחרה איננה תקינה."; "intents.saveFile.missingTemporaryFolder" = "כשלון ביצירת תיקיה זמנית."; diff --git a/SharedResources/hi.lproj/Localizable.strings b/SharedResources/hi.lproj/Localizable.strings index 720614402..4ca6c79d6 100644 --- a/SharedResources/hi.lproj/Localizable.strings +++ b/SharedResources/hi.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "सक्षम करें"; "common.button.next" = "अगला"; "common.button.ok" = "ठीक है"; +"common.button.refresh" = "रीफ्रेश करें"; "common.button.remove" = "हटाएँ"; "common.button.retry" = "पुन: प्रयास करें"; "common.button.signOut" = "साइन आउट करें"; @@ -87,6 +88,7 @@ "fileProvider.error.unlockButton" = "अनलॉक करें"; "fileProvider.clearFileFromCache.title" = "कैशे से फ़ाइल साफ़ करें"; "fileProvider.clearFileFromCache.message" = "यह केवल आपके डिवाइस से स्थानीय फ़ाइल को हटाता है और क्लाउड में फ़ाइल को नहीं हटाता"; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "डिवाइस का नाम"; "localFileSystemAuthentication.createNewVault.header" = "अगले स्क्रीन पर अपने कक्ष के लिए भंडार स्थान का चयन करें।"; "localFileSystemAuthentication.createNewVault.button" = "संग्रहण का स्थान चयन करें"; diff --git a/SharedResources/hr.lproj/Localizable.strings b/SharedResources/hr.lproj/Localizable.strings index 972ac0d67..ef69f4e98 100644 --- a/SharedResources/hr.lproj/Localizable.strings +++ b/SharedResources/hr.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "Omogući"; "common.button.next" = "Sljedeći"; "common.button.ok" = "U redu"; +"common.button.refresh" = "Osvježi"; "common.button.remove" = "Ukloni"; "common.button.retry" = "Pokušaj ponovno"; "common.button.signOut" = "Odjavi se"; @@ -109,6 +110,7 @@ "getFolderIntent.error.missingPath" = "Putanja nije navedena. Navedite valjanu putanju za koju treba vratiti mapu."; "getFolderIntent.error.noVaultSelected" = "Niti jedan trezor nije odabran."; + "intents.saveFile.missingFile" = "Datoteka nije ispravna."; "intents.saveFile.invalidFolder" = "Mapa nije ispravna."; "intents.saveFile.missingTemporaryFolder" = "Izrada privremene mape nije uspjela."; diff --git a/SharedResources/hu.lproj/Localizable.strings b/SharedResources/hu.lproj/Localizable.strings index 69c919ab6..dc87abdcc 100644 --- a/SharedResources/hu.lproj/Localizable.strings +++ b/SharedResources/hu.lproj/Localizable.strings @@ -1,32 +1,193 @@ +/* + Localizable.strings + Cryptomator + + Copyright © 2021 Skymatic GmbH. All rights reserved. +*/ + +"common.alert.error.title" = "Hiba"; "common.alert.attention.title" = "Figyelem"; "common.button.cancel" = "Mégse"; "common.button.change" = "Változtat"; "common.button.choose" = "Választás"; +"common.button.clear" = "Törlés"; "common.button.close" = "Bezár"; +"common.button.confirm" = "Megerősítés"; "common.button.create" = "Létrehozás"; +"common.button.createFolder" = "Mappa létrehozása"; "common.button.done" = "Kész"; +"common.button.download" = "Letöltés"; "common.button.edit" = "Szerkeszt"; "common.button.enable" = "Engedélyezés"; "common.button.next" = "Következő"; "common.button.ok" = "OK"; +"common.button.refresh" = "Frissítés"; +"common.button.register" = "Regisztráció"; "common.button.remove" = "Eltávolítás"; "common.button.retry" = "Újra"; +"common.button.signOut" = "Kijelentkezés"; +"common.button.verify" = "Hitelesítés"; +"common.cells.openInFilesApp" = "Megnyitás ezzel: Fájlok"; "common.cells.password" = "Jelszó"; "common.cells.url" = "ULR"; "common.cells.username" = "Felhasználónév"; +"common.footer.learnMore" = "További információk."; +"common.hud.authenticating" = "Hitelesítés…"; + +"accountList.header.title" = "Hitelesítések"; +"accountList.emptyList.message" = "Koppintson ide egy fiók hozzáadásához"; +"accountList.signOut.alert.title" = "Hozzátartozó széfek eltávolítása?"; +"accountList.signOut.alert.message" = "A kijelentkezéssel az összes hozzátartozó széf eltávolításra kerül a listából. Semmilyen titkosított adat nem törlődik. Később újra bejelentkezhet és újra felveheti a széfeket."; "addVault.title" = "Széf hozzáadása"; "addVault.createNewVault.title" = "Új széf létrehozása"; +"addVault.createNewVault.purchase" = "Új széf létrehozásához meg kell vennie a Cryptomator teljes verzióját."; +"addVault.createNewVault.setVaultName.header.title" = "Válasszon egy nevet az új széf számára."; "addVault.createNewVault.setVaultName.cells.name" = "A széf neve"; +"addVault.createNewVault.setVaultName.error.emptyVaultName" = "A széf neve nem lehet üres."; "addVault.createNewVault.chooseCloud.header" = "Hova mentse a Cryptomator a széf titkosított fájljait?"; +"addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" nevű széf már létezik ezen a helyen. Válasszon másik nevet vagy helyet a széfnek."; +"addVault.createNewVault.detectedMasterkey.text" = "A Cryptomator talált egy már létező széfet ezen a helyen.\nÚj széf létrehozásához lépjen vissza és válasszon egy másik mappát."; +"addVault.createNewVault.password.enterPassword.header" = "Adjon meg egy új jelszót."; +"addVault.createNewVault.password.confirmPassword.header" = "Erősítse meg az új jelszót."; +"addVault.createNewVault.password.confirmPassword.alert.title" = "Jelszó megerősítése?"; "addVault.createNewVault.password.confirmPassword.alert.message" = "FONTOS: Ha elfelejti a jelszavát, nincs mód az adatok helyreállítására."; +"addVault.createNewVault.password.error.emptyPassword" = "A jelszó nem lehet üres."; +"addVault.createNewVault.password.error.nonMatchingPasswords" = "A jelszavak nem egyeznek."; +"addVault.createNewVault.password.error.tooShortPassword" = "A jelszónak legalább 8 karaktert kell tartalmaznia."; +"addVault.createNewVault.progress" = "Széf létrehozása…"; "addVault.openExistingVault.title" = "Meglévő széf megnyitása"; +"addVault.openExistingVault.chooseCloud.header" = "Hol található a széf?"; +"addVault.openExistingVault.detectedMasterkey.text" = "A Cryptomator felismerte a következő széfet: \"%@\".\nSzeretné ezt a széfet hozzáadni?"; +"addVault.openExistingVault.detectedMasterkey.add" = "Széf hozzáadása"; +"addVault.openExistingVault.downloadVault.progress" = "Széf letöltése…"; +"addVault.openExistingVault.password.footer" = "Írja be a jelszót a következőhöz: \"%@\"."; +"addVault.openExistingVault.progress" = "Széf hozzáadása…"; +"addVault.success.info" = "Sikeresen megtörtént a következő széf hozzáadása: \"%@\".\nElérheti ezt a széfet a Fájlok appban."; +"addVault.success.footer" = "Ha még nem tette volna, engedélyezze a Cryptomatort a Fájlok appban."; + +"biometryType.faceID" = "Face ID"; +"biometryType.touchID" = "Touch ID"; + +"changePassword.error.invalidOldPassword" = "A jelenlegi jelszó helytelen. Kérjük, próbálja újra."; +"changePassword.header.currentPassword.title" = "Írja be a jelenlegi jelszót."; +"changePassword.header.newPassword.title" = "Adjon meg egy új jelszót."; +"changePassword.header.newPasswordConfirmation.title" = "Erősítse meg az új jelszót."; +"changePassword.progress" = "Jelszó módosítása…"; + +"chooseFolder.emptyFolder.footer" = "A mappa üres"; +"chooseFolder.createNewFolder.header.title" = "Válasszon egy nevet a mappa számára."; +"chooseFolder.createNewFolder.cells.name" = "Mappa neve"; +"chooseFolder.createNewFolder.error.emptyFolderName" = "A mappa neve nem lehet üres."; +"chooseFolder.createNewFolder.progress" = "Mappa létrehozása…"; + +"cloudProvider.error.itemNotFound" = "A(z) \"%@\" nem található."; +"cloudProvider.error.itemAlreadyExists" = "A(z) \"%@\" már létezik."; +"cloudProvider.error.itemTypeMismatch" = "A következőben egy nem várt elemtípus található: \"%@\"."; +"cloudProvider.error.parentFolderDoesNotExist" = "A szülőmappa \"%@\" nem létezik."; +"cloudProvider.error.pageTokenInvalid" = "A könyvtár elemeinek lekérését nem tudtuk folytatni."; +"cloudProvider.error.quotaInsufficient" = "Kevés a tárhely."; +"cloudProvider.error.unauthorized" = "Nem lehet jogosulatlan műveletet végrehajtani."; +"cloudProvider.error.noInternetConnection" = "Internet hozzáférés szükséges ehhez a művelethez."; + +"cloudProviderType.localFileSystem" = "Más fájlszolgáltató"; + +"fileProvider.onboarding.title" = "Üdvözöljük"; +"fileProvider.onboarding.info" = "Köszönjük, hogy a Cryptomatort válaszotta fájlainak megvédéséhez. Kezdéshez menjen a fő appba és adjon hozzá egy széfet."; +"fileProvider.onboarding.button.openCryptomator" = "Cryptomator megnyitása"; +"fileProvider.error.biometricalAuthCanceled.title" = "Feloldás megszakítva"; +"fileProvider.error.biometricalAuthWrongPassword.title" = "Helytelen jelszó"; +"fileProvider.error.defaultLock.title" = "Feloldás szükséges"; "fileProvider.error.unlockButton" = "Feloldás"; +"fileProvider.clearFileFromCache.title" = "Fájl törlése az átmeneti tárból"; +"fileProvider.clearFileFromCache.message" = "Ez csak a helyi fájlt távolítja el az eszközről, de nem törli a fájlt a felhőben."; +"fileProvider.fileImporting.error.missingPremium" = "Oldja fel a teljes verziót a Cryptomator alkalmazásban, hogy írási hozzáférést kapjon a széfjeihez."; +"fileProvider.uploadProgress.connecting" = "Kapcsolódás…"; +"fileProvider.uploadProgress.title" = "Feltöltés…"; +"fileProvider.uploadProgress.missingDomainError" = "A domain nem található."; +"getFolderIntent.error.noVaultSelected" = "Nincs széf kiválasztva."; +"hubAuthentication.accessNotGranted" = "Eszköze még nem kapott engedélyt ehhez a széfhez. Kérje a széf tulajdonosát, hogy engedélyezze a hozzáférést."; +"hubAuthentication.licenseExceeded" = "Az Ön Cryptomator Hub példánya érvénytelen licenccel rendelkezik. Kérem, értesítsen egy Hub rendszergazdát hogy frissítse vagy újítsa meg a licencet."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Készülék neve"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Az eszköz regisztrációja sikeres"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "A széf hozzáféréséhez a széf tulajdonosának hitelesítenie kell az eszközét."; + +"intents.saveFile.missingFile" = "A megadott fájl érvénytelen."; +"intents.saveFile.invalidFolder" = "A megadott mappa érvénytelen."; +"intents.saveFile.missingTemporaryFolder" = "Nem sikerült létrehozni az ideiglenes mappát."; + +"keepUnlocked.alert.title" = "Széf zárolása?"; +"keepUnlocked.alert.message" = "Ezen módosítások végbemenéséhez szükséges a széfet zárolni."; +"keepUnlocked.alert.confirm" = "Megerősítés és zárolás most"; +"keepUnlocked.header" = "Adja meg, hogy mennyi ideig legyen a széf feloldva amikor tétlen."; +"keepUnlockedDuration.auto" = "Döntsön az iOS automatikusan"; +"keepUnlockedDuration.auto.shortDisplayName" = "Automatikus"; +"keepUnlockedDuration.indefinite" = "Korlátlan"; + +"onboarding.title" = "Üdvözöljük"; +"onboarding.info" = "Köszönjük, hogy a Cryptomatort választotta fájlai védelméhez.\n\nA Cryptomatorral az adatainak kulcsa az ön kezében van. A Cryptomator titkosítja az adatait gyorsan és könnyedén.\n\nEz az alkalmazás teljesen integrálva van a Fájlok appba. Engedélyezze a Cryptomatort a Fájlok appban később, hogy hozzáférhessen a széfjeihez."; +"onboarding.button.continue" = "Tovább"; + +"purchase.beginFreeTrial.alert.title" = "Próbaverzió feloldva"; +"purchase.expiredTrial" = "A próbaidőszak lejárt."; +"purchase.footer.privacyPolicy" = "Adatvédelmi irányelvek"; +"purchase.footer.termsOfUse" = "Használati feltételek"; +"purchase.header.feature.familySharing" = "Családon belüli megosztás"; +"purchase.header.feature.openSource" = "Nyílt forráskódú fejlesztés"; +"purchase.product.donateAndUpgrade" = "Adományozás és frissítés"; +"purchase.product.freeUpgrade" = "Ingyenes frissítés"; +"purchase.product.lifetimeLicense" = "Örök licensz"; +"purchase.product.lifetimeLicense.duration" = "egyszeri"; +"purchase.product.pricing.free" = "Ingyenes"; +"purchase.product.trial" = "30 napos próbaverzió"; +"purchase.product.trial.expirationDate" = "Lejárati dátum: %@"; +"purchase.product.trial.duration" = "30 napig"; +"purchase.product.yearlySubscription" = "Éves előfizetés"; +"purchase.product.yearlySubscription.duration" = "évente"; +"purchase.readOnlyMode.alert.title" = "Csak olvasható mód"; +"purchase.readOnlyMode.alert.message" = "Feloldhatja a Cryptomator teljes verzióját a beállításokban később és addig használhatja csak olvasható módban."; +"purchase.restorePurchase.button" = "Vásárlás visszaállítása"; +"purchase.restorePurchase.validTrialFound.alert.title" = "Próbaidő folytatódott"; +"purchase.restorePurchase.validTrialFound.alert.message" = "Mostmár használhatja a Cryptomator teljes verzióját korlátozott időre. A próbaidője lejár ekkor: %@. Azután a széfeit továbbra is elérheti csak olvasható módban."; +"purchase.restorePurchase.fullVersionFound.alert.title" = "Sikeres visszaállítás"; +"purchase.restorePurchase.fullVersionNotFound.alert.title" = "Nincs teljes verzió"; +"purchase.restorePurchase.fullVersionNotFound.alert.message" = "Nem találtunk egy előzőleg megvásárolt teljes verziót, melyet visszaállíthattunk volna. Kérjük, próbáljon meg egy másik opciót."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Frissítésre jogosult"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Úgy tűnik, hogy a Cryptomator egy régebbi verziójáról próbál meg frissíteni. Ebben az esetben, kérjük válassza a \"Frissítési ajánlat\" opciót."; "purchase.retry.button" = "Újra"; +"purchase.retry.footer" = "Az elérhető termékek betöltése sikertelen."; +"purchase.title" = "Teljes verzió feloldása"; +"purchase.unlockedFullVersion.message" = "Mostmár használhatja a Cryptomator teljes verzióját. Örömteli titkosítást!"; +"purchase.unlockedFullVersion.title" = "Köszönjük"; +"purchase.error.unknown" = "A vásárlás ismeretlen ok miatt nem elérhető az App Storeban. Kérjük, próbálja újra később.\n\nHa ez a probléma fennállna, próbálja meg újraindítani a készülékét vagy ki- és bejelentkezni az Apple ID-ból az iOS beállításokban."; "settings.title" = "Beállítások"; +"settings.aboutCryptomator" = "A Cryptomator névjegye"; +"settings.aboutCryptomator.title" = "Verzió %@ (%@)"; +"settings.cacheSize" = "Gyorsítótár mérete"; "settings.clearCache" = "Gyorsítótár törlése"; +"settings.cloudServices" = "Felhőszolgáltatások"; +"settings.contact" = "Kapcsolat"; +"settings.debugMode" = "Hibakeresési mód"; "settings.debugMode.alert.message" = "Ebben a módban érzékeny adatok kerülhetnek az eszközön lévő naplófájlba (pl. fájlnevek és -útvonalak). Ez jelszavakra és sütikre nem vonatkozik.\n\n Ne felejtse el minél hamarabb kikapcsolni a fejlesztői módot."; +"settings.manageSubscriptions" = "Előfizetés kezelése"; +"settings.rateApp" = "Alkalmazás értékelése"; +"settings.sendLogFile" = "Naplófájl küldése"; +"settings.shortcutsGuide" = "Parancsok útmutató"; +"settings.unlockFullVersion" = "Teljes verzió feloldása"; + +"snapshots.fileprovider.file1" = "/Könyevlés.numbers"; +"snapshots.fileprovider.file2" = "/Kész Előadás.key"; +"snapshots.fileprovider.file3" = "/Termék Filmelőzetes.mov"; +"snapshots.fileprovider.file4" = "/Javaslat.docx"; +"snapshots.fileprovider.file5" = "/Jelentés.pdf"; +"snapshots.fileprovider.folder3" = "/Titkos Projekt"; +"snapshots.fileprovider.folder2" = "/Számlák"; +"snapshots.fileprovider.folder1" = "/Tanúsítványok"; +"snapshots.main.vault1" = "/Munka"; +"snapshots.main.vault2" = "/Család"; +"snapshots.main.vault3" = "/Dokumentumok"; +"snapshots.main.vault4" = "/Utazás Kaliforniába"; "s3Authentication.displayName" = "Megjelenítendő név"; "s3Authentication.accessKey" = "Hozzáférési kulcs"; @@ -34,16 +195,43 @@ "s3Authentication.existingBucket" = "Meglévő tartály"; "s3Authentication.endpoint" = "Végpont"; "s3Authentication.region" = "Régió"; +"s3Authentication.error.invalidCredentials" = "Érvénytelen hitelesítő adatok."; +"s3Authentication.error.invalidEndpoint" = "A megadott végpont nem egyezik az URL formátumával."; + +"trialStatus.active" = "Aktív"; +"trialStatus.expired" = "Lejárt"; "unlockVault.button.unlock" = "Kinyitás"; "unlockVault.button.unlockVia" = "Kinyitva %@ által"; +"unlockVault.password.footer" = "Írja be a jelszót a következőhöz: \"%@\"."; "unlockVault.enableBiometricalUnlock.switch" = "%@ engedélyezése"; "unlockVault.enableBiometricalUnlock.footer" = "Ahelyett, hogy jelszavával nyitná fel a széfet, a %@ segítségével is feloldhatja azt."; "unlockVault.evaluatePolicy.reason" = "Széf kinyitása"; +"unlockVault.progress" = "Feloldás…"; + +"untrustedTLSCertificate.title" = "Érvénytelen TLS-tanúsítvány"; +"untrustedTLSCertificate.message" = "A \"%@\" TLS-tanúsítványa érvénytelen. Megjelöli mégis megbízhatóként?\n\n SHA-256: %@"; +"untrustedTLSCertificate.add" = "Megbízható"; +"untrustedTLSCertificate.dismiss" = "Nem megbízható"; + +"upgrade.title" = "Frissítési ajánlat"; +"upgrade.notEligible.alert.title" = "A frissítés nem sikerült"; +"urlSession.error.httpError.404" = "A keresett oldal nem található."; "vaultDetail.button.changeVaultPassword" = "Jelszó megváltoztatása"; +"vaultDetail.button.lock" = "Zárolás most"; "vaultDetail.button.moveVault" = "Áthelyezés"; "vaultDetail.button.renameVault" = "Átnevezés"; +"vaultDetail.info.footer.accessVault" = "Elérheti a széfet a Fájlok appban."; +"vaultDetail.info.footer.accountInfo" = "Bejelentkezve, mint %@ itt: %@."; +"vaultDetail.keepUnlocked.title" = "Feloldás hossza"; +"vaultDetail.moveVault.progress" = "Áthelyezés…"; +"vaultDetail.renameVault.progress" = "Átnevezés…"; +"vaultDetail.unlocked.footer" = "A széfe jelenleg fel van oldva a Fájlok appban."; + +"vaultList.header.title" = "Széfek"; +"vaultList.emptyList.message" = "Koppintson ide egy széf hozzáadásához"; +"vaultList.remove.alert.title" = "Széf eltávolitása?"; "webDAVAuthentication.httpConnection.alert.title" = "HTTPS használata"; "webDAVAuthentication.httpConnection.alert.message" = "A HTTP használata nem biztonságos, helyette a HTTPS használata ajánlott. Ha ismeri a kockázatot, folytathatja HTTP-vel is."; diff --git a/SharedResources/id.lproj/Localizable.strings b/SharedResources/id.lproj/Localizable.strings index 0018f1ca7..d915210e4 100644 --- a/SharedResources/id.lproj/Localizable.strings +++ b/SharedResources/id.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "Aktifkan"; "common.button.next" = "Lanjut"; "common.button.ok" = "OK"; +"common.button.refresh" = "Segarkan"; "common.button.remove" = "Hapus"; "common.button.retry" = "Coba lagi"; "common.button.signOut" = "Keluar"; @@ -109,6 +110,7 @@ "getFolderIntent.error.missingPath" = "Tidak ada path yang disediakan. Harap cantumkan path yang valid untuk folder yang harus dikembalikan."; "getFolderIntent.error.noVaultSelected" = "Tidak ada vault yang dipilih."; + "intents.saveFile.missingFile" = "File yang disediakan tidak valid."; "intents.saveFile.invalidFolder" = "Folder yang disediakan tidak valid."; "intents.saveFile.missingTemporaryFolder" = "Gagal membuat folder sementara."; diff --git a/SharedResources/it.lproj/Localizable.strings b/SharedResources/it.lproj/Localizable.strings index 8b013eafa..818c4139c 100644 --- a/SharedResources/it.lproj/Localizable.strings +++ b/SharedResources/it.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Abilita"; "common.button.next" = "Avanti"; "common.button.ok" = "OK"; +"common.button.refresh" = "Aggiorna"; +"common.button.register" = "Registrati"; "common.button.remove" = "Rimuovi"; "common.button.retry" = "Riprova"; "common.button.signOut" = "Disconnettiti"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Dove si trova la cassaforte?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator ha rilevato la cassaforte \"%@\".\nVorresti aggiungerla?"; "addVault.openExistingVault.detectedMasterkey.add" = "Aggiungi Questa Cassaforte"; +"addVault.openExistingVault.downloadVault.progress" = "Scaricamento Cassaforte…"; "addVault.openExistingVault.password.footer" = "Inserisci la password per \"%@\"."; "addVault.openExistingVault.progress" = "Aggiungendo la Cassaforte…"; "addVault.success.info" = "Cassaforte \"%@\" aggiunta correttamente.\nAccedi a questa cassaforte tramite l'app File."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Non è stato fornito alcun percorso. Fornire un percorso valido per il quale deve essere restituita una cartella."; "getFolderIntent.error.noVaultSelected" = "Nessuna cassaforte è stata selezionata."; + +"hubAuthentication.title" = "Centrale delle Casseforti"; +"hubAuthentication.accessNotGranted" = "Il tuo dispositivo non è ancora stato autorizzato ad accedere a questa cassaforte. Chiedi al proprietario della cassaforte di autorizzarlo."; +"hubAuthentication.licenseExceeded" = "La tua istanza Cryptomator Hub ha una licenza non valida. Si prega di informare un amministratore Hub per aggiornare o rinnovare la licenza."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nome Del Dispositivo"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Sembra che questo sia il primo accesso alle Centrali delle Casseforti da questo dispositivo. Per poterlo identificare ai fini dell'autorizzazione all'accesso è necessario dare un nome a questo dispositivo."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "La chiave del tuo account è richiesta per accedere da nuove applicazioni o browser. Può essere trovata nel tuo profilo."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Registrazione del dispositivo Riuscita"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Per accedere al vault, il tuo dispositivo deve essere autorizzato dal proprietario del vault."; +"hubAuthentication.requireAccountInit.alert.title" = "Azione richiesta"; +"hubAuthentication.requireAccountInit.alert.message" = "Per procedere, completa i passaggi richiesti nel tuo profilo dell'Hub."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Vai al profilo"; + "intents.saveFile.missingFile" = "Il file fornito non è valido."; "intents.saveFile.invalidFolder" = "La cartella fornita non è valida."; "intents.saveFile.missingTemporaryFolder" = "Non è stato possibile creare la cartella temporanea."; diff --git a/SharedResources/ja.lproj/Localizable.strings b/SharedResources/ja.lproj/Localizable.strings index a4724c7fb..8f4ecf5eb 100644 --- a/SharedResources/ja.lproj/Localizable.strings +++ b/SharedResources/ja.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "有効にする"; "common.button.next" = "次へ"; "common.button.ok" = "OK"; +"common.button.refresh" = "更新"; +"common.button.register" = "登録"; "common.button.remove" = "削除"; "common.button.retry" = "再試行"; "common.button.signOut" = "サインアウト"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "金庫の場所"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomatorが金庫 \"%@\" を検出しました。\nこの金庫を追加しますか?"; "addVault.openExistingVault.detectedMasterkey.add" = "この金庫を追加"; +"addVault.openExistingVault.downloadVault.progress" = "金庫をダウンロード…"; "addVault.openExistingVault.password.footer" = "\"%@\" のパスワードを入力してください。"; "addVault.openExistingVault.progress" = "金庫を追加しています…"; "addVault.success.info" = "金庫 \"%@\" を正常に追加しました。\nファイル アプリからこの金庫にアクセスしてください。"; @@ -110,6 +113,15 @@ "getFolderIntent.error.missingPath" = "パスがありません。妥当なフォルダーのパスを指定してください。"; "getFolderIntent.error.noVaultSelected" = "金庫は選択されていません。"; +"hubAuthentication.accessNotGranted" = "お使いのデバイスはまだこの金庫にアクセスする権限がありません。金庫のオーナーに権限を与えてもらってください。"; +"hubAuthentication.licenseExceeded" = "Cryptomator Hub インスタンスのライセンスが無効です。ライセンスをアップグレードまたは更新するには、Hub の管理者にご連絡ください。"; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "デバイス名"; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "アカウントキーは新しいアプリやブラウザからログインするために必要です。プロフィール中に記載されています。"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "金庫にアクセスするためには,金庫のオーナーが端末を認証する必要があります。"; +"hubAuthentication.requireAccountInit.alert.title" = "要対応"; +"hubAuthentication.requireAccountInit.alert.message" = "続行するにはHubユーザープロフィールで必要な手順を完了してください。"; +"hubAuthentication.requireAccountInit.alert.actionButton" = "プロフィールへ移動"; + "intents.saveFile.missingFile" = "指定されたファイルはだめです。"; "intents.saveFile.invalidFolder" = "指定されたフォルダーはだめです。"; "intents.saveFile.missingTemporaryFolder" = "臨時フォルダーの作成ができませんでした。"; diff --git a/SharedResources/ko.lproj/Localizable.strings b/SharedResources/ko.lproj/Localizable.strings index 0014858fa..380f92695 100644 --- a/SharedResources/ko.lproj/Localizable.strings +++ b/SharedResources/ko.lproj/Localizable.strings @@ -15,10 +15,12 @@ "common.button.create" = "생성"; "common.button.createFolder" = "폴더 생성"; "common.button.done" = "완료"; +"common.button.download" = "다운로드"; "common.button.edit" = "수정"; "common.button.enable" = "활성화"; "common.button.next" = "다음"; "common.button.ok" = "OK"; +"common.button.refresh" = "새로고침"; "common.button.remove" = "제거"; "common.button.retry" = "재시도"; "common.button.signOut" = "로그아웃"; @@ -28,6 +30,9 @@ "common.cells.url" = "URL"; "common.cells.username" = "사용자명"; "common.footer.learnMore" = "자세히 알아보기"; +"common.hud.authenticating" = "로그인중…"; + +"accountList.header.title" = "계정"; "accountList.emptyList.message" = "여기를 눌러 계정 추가"; "accountList.signOut.alert.title" = "연결된 Vault를 제거하시겠습니까?"; "accountList.signOut.alert.message" = "로그아웃 시, 연결된 Vault가 목록에서 모두 제거됩니다. 암호화된 데이터가 삭제되는 것은 아닙니다. 다시 로그인 시 Vault를 다시 추가 할 수 있습니다."; @@ -37,7 +42,9 @@ "addVault.createNewVault.purchase" = "새로운 vault를 만들려면 정식 버전을 구입해야 합니다."; "addVault.createNewVault.setVaultName.header.title" = "Vault의 이름을 선택해주십시요."; "addVault.createNewVault.setVaultName.cells.name" = "Vault 이름"; +"addVault.createNewVault.setVaultName.error.emptyVaultName" = "Vault 이름은 비어있을 수 없습니다."; "addVault.createNewVault.chooseCloud.header" = "Cryptomator Vault의 암호화 파일을 어디에 저장하시겠습니까?"; +"addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "해당 위치에 \"%@\"가 이미 존재합니다. 다른 vault명을 정하거나 다른 위치를 선택해주세요."; "addVault.createNewVault.detectedMasterkey.text" = "Cryptomator가 이 위치에 있는 Vault를 감지하였습니다.\n 새 Vault를 생성하기 위해, 다른 경로를 선택하시기 바랍니다."; "addVault.createNewVault.password.enterPassword.header" = "새 비밀번호를 입력하세요."; "addVault.createNewVault.password.confirmPassword.header" = "새 비밀번호 확인."; @@ -60,6 +67,7 @@ "biometryType.touchID" = "Touch ID"; "changePassword.error.invalidOldPassword" = "기존 비밀번호가 올바르지 않습니다. 다시 시도하십시오."; +"changePassword.header.currentPassword.title" = "기존 비밀번호를 입력해주세요."; "changePassword.header.newPassword.title" = "새 비밀번호를 입력하세요."; "changePassword.header.newPasswordConfirmation.title" = "새 비밀번호 확인."; "changePassword.progress" = "비밀번호 변경 중..."; @@ -70,6 +78,12 @@ "chooseFolder.createNewFolder.error.emptyFolderName" = "폴더 이름이 비어 있어서는 안 됩니다."; "chooseFolder.createNewFolder.progress" = "폴더 생성 중..."; +"cloudProvider.error.itemNotFound" = "\"%@\"를 찾을 수 없습니다."; +"cloudProvider.error.itemAlreadyExists" = "\"%@\"가 이미 존재합니다."; +"cloudProvider.error.itemTypeMismatch" = "\"%@\"의 파일 타입이 올바르지 않습니다."; +"cloudProvider.error.parentFolderDoesNotExist" = "상위 폴더 \"%@\"가 존재하지 않습니다."; +"cloudProvider.error.quotaInsufficient" = "저장공간이 부족합니다."; + "cloudProviderType.localFileSystem" = "다른 파일 앱"; "fileProvider.onboarding.title" = "환영합니다"; @@ -79,6 +93,10 @@ "fileProvider.fileImporting.error.missingPremium" = "Vault에 쓰기 권한을 얻기 위해 Cryptomator앱에서 정식 버전을 구입하십시오."; "fileProvider.uploadProgress.connecting" = "연결 중..."; "fileProvider.uploadProgress.title" = "업로드 중…"; +"hubAuthentication.accessNotGranted" = "귀하의 기기는 아직 이 저장소에 액세스할 수 있는 권한이 없습니다. Vault 소유자에게 승인을 요청하세요."; +"hubAuthentication.licenseExceeded" = "Cryptomator Hub 인스턴스에 잘못된 라이선스가 있습니다. 라이센스를 업그레이드하거나 갱신하려면 허브 관리자에게 알리십시오."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "기기 이름"; + "intents.saveFile.missingFile" = "파일이 올바르지 않습니다."; "intents.saveFile.invalidFolder" = "폴더가 올바르지 않습니다."; @@ -121,6 +139,7 @@ "s3Authentication.displayName" = "표시 이름"; "s3Authentication.accessKey" = "액세스 키"; "s3Authentication.secretKey" = "비밀키"; +"s3Authentication.existingBucket" = "이미 존재하는 버킷"; "s3Authentication.endpoint" = "엔드포인트"; "s3Authentication.region" = "지역"; @@ -138,6 +157,7 @@ "vaultDetail.button.changeVaultPassword" = "비밀번호 변경"; "vaultDetail.button.moveVault" = "이동"; "vaultDetail.button.renameVault" = "이름 변경"; +"vaultDetail.renameVault.progress" = "이름변경중…"; "vaultList.header.title" = "Vault"; "vaultList.emptyList.message" = "여기를 눌러 Vault 추가"; @@ -147,3 +167,5 @@ "webDAVAuthentication.httpConnection.alert.title" = "HTTPS를 사용하겠습니가?"; "webDAVAuthentication.httpConnection.alert.message" = "HTTP 사용은 안전하지 않습니다. HTTPS 사용을 권장합니다. HTTP 사용으로 야기될 문제를 숙지하고 있다면, HTTP를 사용할 수 있습니다."; "webDAVAuthentication.httpConnection.change" = "HTTPS로 변경"; + +"Retry Upload" = "업로드 재시도"; diff --git a/SharedResources/lv.lproj/Localizable.strings b/SharedResources/lv.lproj/Localizable.strings index da3698ff5..0b1101d65 100644 --- a/SharedResources/lv.lproj/Localizable.strings +++ b/SharedResources/lv.lproj/Localizable.strings @@ -11,6 +11,7 @@ "addVault.createNewVault.chooseCloud.header" = "Kur Cryptomator vajadzētu glabāt jūsu glabātuves šifrētos failus?"; "addVault.openExistingVault.title" = "Atvērt esošu glabātuvi"; "fileProvider.error.unlockButton" = "Atslēgt"; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Ierīces nosaukums"; "unlockVault.button.unlock" = "Atslēgt"; diff --git a/SharedResources/mk.lproj/Localizable.strings b/SharedResources/mk.lproj/Localizable.strings index 60f7ef444..cdd508edd 100644 --- a/SharedResources/mk.lproj/Localizable.strings +++ b/SharedResources/mk.lproj/Localizable.strings @@ -7,7 +7,9 @@ "common.alert.error.title" = "Грешка"; "common.button.cancel" = "Излез"; +"common.button.change" = "Промени"; "common.button.choose" = "Избор"; +"common.button.close" = "Затвори"; "common.button.confirm" = "Потврди"; "common.button.create" = "Создади"; "common.button.createFolder" = "Креирај папка"; @@ -37,3 +39,7 @@ "addVault.createNewVault.setVaultName.error.emptyVaultName" = "Името не може да биде празно."; "addVault.createNewVault.chooseCloud.header" = "Каде би сакале Cryptomator да ги зачува шифрираните фајлови на Вашиот сеф?"; "addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" веќе постои на оваа локација. Изберете друго име за сефот или нова локација."; +"addVault.openExistingVault.title" = "Отвори постоечки сеф"; +"fileProvider.error.unlockButton" = "Отклучи"; + +"unlockVault.button.unlock" = "Отклучи"; diff --git a/SharedResources/nb.lproj/Localizable.strings b/SharedResources/nb.lproj/Localizable.strings index bbf46383a..97b5dbe8e 100644 --- a/SharedResources/nb.lproj/Localizable.strings +++ b/SharedResources/nb.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Aktiver"; "common.button.next" = "Neste"; "common.button.ok" = "Ok"; +"common.button.refresh" = "Oppdater"; +"common.button.register" = "Registrer"; "common.button.remove" = "Fjern"; "common.button.retry" = "Prøv igjen"; "common.button.signOut" = "Logg ut"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Hvor er hvelvet plassert?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator oppdaget hvelvet \"%@\".\nØnsker du å legge til dette hvelvet?"; "addVault.openExistingVault.detectedMasterkey.add" = "Legg til dette hvelvet"; +"addVault.openExistingVault.downloadVault.progress" = "Laster ned hvelv…"; "addVault.openExistingVault.password.footer" = "Skriv inn passordet for \"%@\"."; "addVault.openExistingVault.progress" = "Legger til hvelv…"; "addVault.success.info" = "Hvelvet \"%@\" ble lagt til.\nFå tilgang til dette hvelvet via Filer-appen."; @@ -110,6 +113,15 @@ "getFolderIntent.error.missingPath" = "Ingen målmappe ble oppgitt. Vennligst oppgi en gyldig bane hvor en mappe skal returneres til."; "getFolderIntent.error.noVaultSelected" = "Ingen hvelv har blitt valgt."; + +"hubAuthentication.title" = "Hub hvelv"; +"hubAuthentication.accessNotGranted" = "Enheten din har ikke blitt autorisert til å få tilgang til dette hvelvet ennå. Spør hvelveieren om å tillate det."; +"hubAuthentication.licenseExceeded" = "Cryptomator Hub instansen din har en ugyldig lisens. Vennligst informer en Hub-administrator om å oppgradere eller fornye lisensen."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Enhetsnavn"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Dette ser ut til å være den første Hub-tilgangen fra denne enheten. For å kunne identifisere den for tilgangsautorisasjon, må du å navngi denne enheten."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Enhetsregistrering vellykket"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "For å få tilgang til hvelvet, så må enheten din bli autorisert av hvelvets eier."; + "intents.saveFile.missingFile" = "Den oppgitte filen er ugyldig."; "intents.saveFile.invalidFolder" = "Den oppgitte mappen er ugyldig."; "intents.saveFile.missingTemporaryFolder" = "Kunne ikke opprette temporær mappe."; diff --git a/SharedResources/nl.lproj/Localizable.strings b/SharedResources/nl.lproj/Localizable.strings index be59af1b6..ea47789dd 100644 --- a/SharedResources/nl.lproj/Localizable.strings +++ b/SharedResources/nl.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Inschakelen"; "common.button.next" = "Volgende"; "common.button.ok" = "Ok"; +"common.button.refresh" = "Vernieuwen"; +"common.button.register" = "Registreren"; "common.button.remove" = "Verwijderen"; "common.button.retry" = "Opnieuw proberen"; "common.button.signOut" = "Uitloggen"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Wat is de locatie van de kluis?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator detecteerde de kluis \"%@\".\nWilt u deze kluis toevoegen?"; "addVault.openExistingVault.detectedMasterkey.add" = "Voeg deze kluis toe"; +"addVault.openExistingVault.downloadVault.progress" = "Kluis downloaden…"; "addVault.openExistingVault.password.footer" = "Voer wachtwoord voor \"%@\" in."; "addVault.openExistingVault.progress" = "Kluis toevoegen…"; "addVault.success.info" = "Kluis succesvol toegevoegd \"%@\".\nToegang tot deze kluis via de bestandsapp."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Er is geen pad opgegeven. Geef een geldig pad om de folder weer te geven."; "getFolderIntent.error.noVaultSelected" = "Er is geen kluis geselecteerd."; + +"hubAuthentication.title" = "Hub Kluis"; +"hubAuthentication.accessNotGranted" = "Uw toestel is nog niet gemachtigd om toegang te krijgen tot deze kluis. Vraag de eigenaar van de kluis om toestemming te geven."; +"hubAuthentication.licenseExceeded" = "Uw Cryptomator Hub installatie heeft een ongeldige licentie. Contacteer een Hub administrator om de licentie te upgraden of te verlengen."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Naam van toestel"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Dit lijkt de eerste Hub toegang te zijn vanaf dit toestel. Om dit toestel te autoriseren voor toegang, moet u dit toestel benoemen."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Uw accountsleutel is vereist om in te loggen vanuit nieuwe apps of browsers. Deze kan worden gevonden in uw profiel."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Registratie van toestel gelukt"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Om toegang te krijgen tot de kluis, moet je toestel worden gemachtigd door de eigenaar van de kluis."; +"hubAuthentication.requireAccountInit.alert.title" = "Actie vereist"; +"hubAuthentication.requireAccountInit.alert.message" = "Om verder te gaan, gelieve de stappen te voltooien in uw Hub-gebruikersprofiel."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Ga naar Profiel"; + "intents.saveFile.missingFile" = "Het opgegeven bestand is niet geldig."; "intents.saveFile.invalidFolder" = "De opgegeven map is niet geldig."; "intents.saveFile.missingTemporaryFolder" = "Aanmaken van tijdelijke map mislukt."; diff --git a/SharedResources/pa.lproj/Localizable.strings b/SharedResources/pa.lproj/Localizable.strings index 52494d230..093d84b5d 100644 --- a/SharedResources/pa.lproj/Localizable.strings +++ b/SharedResources/pa.lproj/Localizable.strings @@ -1,18 +1,196 @@ +/* + Localizable.strings + Cryptomator + + Copyright © 2021 Skymatic GmbH. All rights reserved. +*/ + +"common.alert.error.title" = "ਗਲਤੀ"; +"common.alert.attention.title" = "ਧਿਆਨ ਦਿਓ"; "common.button.cancel" = "ਰੱਦ ਕਰੋ"; "common.button.change" = "ਬਦਲੋ"; "common.button.choose" = "ਚੁਣੋ"; +"common.button.clear" = "ਮਿਟਾਓ"; "common.button.close" = "ਬੰਦ ਕਰੋ"; +"common.button.confirm" = "ਤਸਦੀਕ"; +"common.button.create" = "ਬਣਾਓ"; +"common.button.createFolder" = "ਫੋਲਡਰ ਬਣਾਓ"; "common.button.done" = "ਮੁਕੰਮਲ"; +"common.button.download" = "ਡਾਊਨਲੋਡ"; +"common.button.edit" = "ਸੋਧੋ"; +"common.button.enable" = "ਸਮਰੱਥ"; "common.button.next" = "ਅੱਗੇ"; +"common.button.ok" = "ਠੀਕ ਹੈ"; +"common.button.remove" = "ਹਟਾਓ"; +"common.button.retry" = "ਮੁੜ-ਕੋਸ਼ਿਸ਼"; +"common.button.signOut" = "ਸਾਈਨ ਆਉਟ"; +"common.button.verify" = "ਤਸਦੀਕ"; +"common.cells.openInFilesApp" = "Files ਐਪ ਵਿੱਚ ਖੋਲ੍ਹੋ"; "common.cells.password" = "ਪਾਸਵਰਡ"; +"common.cells.url" = "URL"; +"common.cells.username" = "ਵਰਤੋਂਕਾਰ ਨਾਂ"; +"common.footer.learnMore" = "ਹੋਰ ਜਾਣੋ।"; +"common.hud.authenticating" = "ਪਰਮਾਣਿਤ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…"; + +"accountList.header.title" = "ਪਰਮਾਣਕਿਤਾ"; +"accountList.emptyList.message" = "ਖਾਤਾ ਜੋੜਣ ਲਈ ਇੱਥੇ ਛੋਹੋ"; +"accountList.signOut.alert.title" = "ਸੰਬੰਧਿਤ ਖਾਤੇ ਹਟਾਉਣੇ ਹਨ?"; +"accountList.signOut.alert.message" = "ਸਾਈਨ ਆਉਟ ਕਰਨ ਨਾਲ ਵਾਲਟ ਸੂਚੀ ਵਿੱਚੋਂ ਸਾਰੇ ਸੰਬੰਧਿਤ ਵਾਲਟ ਹਟਾਏ ਜਾਣਗੇ। ਕੋਈ ਵੀ ਇੰਕ੍ਰਿਪਟ ਕੀਤਾ ਡਾਟੇ ਨਹੀਂ ਹਟਾਇਆ ਜਾਵੇਗਾ। ਤੁਸੀਂ ਮੁੜ ਸਾਈਨ ਕਰ ਸਕਦੇ ਹੋ ਅਤੇ ਵਾਲਟਾਂ ਨੂੰ ਫੇਰ ਜੋੜ ਸਕਦੇ ਹੋ।"; "addVault.title" = "ਵਾਲਟ ਜੋੜੋ"; "addVault.createNewVault.title" = "ਨਵਾਂ ਵਾਲਟ ਬਣਾਓ"; +"addVault.createNewVault.purchase" = "ਨਵਾਂ ਵਾਲਟ ਬਣਾਉਣ ਥਈ Cryptomator ਦਾ ਪੂਰਾ ਵਰਜ਼ਨ ਚਾਹੀਦਾ ਹੈ।"; +"addVault.createNewVault.setVaultName.header.title" = "ਵਾਲਟ ਲਈ ਨਵਾਂ ਚੁਣੋ।"; "addVault.createNewVault.setVaultName.cells.name" = "ਵਾਲਟ ਦਾ ਨਾਂ"; +"addVault.createNewVault.setVaultName.error.emptyVaultName" = "ਵਾਲਟ ਨਾਂ ਖਾਲੀ ਨਹੀਂ ਹੋ ਸਕਦਾ ਹੈ।"; "addVault.createNewVault.chooseCloud.header" = "ਤੁਹਾਡੇ ਵਾਲੇਟ ਲਈ ਇੰਕ੍ਰਿਪਟ ਕੀਤੀਆਂ ਫ਼ਾਇਲਾਂ Cryptomator ਕਿੱਥੇ ਸਟੋਰ ਕਰੇ?"; +"addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "ਇਸ ਟਿਕਾਣੇ ਉੱਤੇ \"%@\" ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ। ਵੱਖਰਾ ਵਾਲਟ ਨਾਂ ਜਾਂ ਟਿਕਾਣਾ ਚੁਣੋ।"; +"addVault.createNewVault.detectedMasterkey.text" = "Cryptomator ਨੂੰ ਇਸ ਟਿਕਾਣੇ ਉੱਤੇ ਇੱਕ ਮੌਜੂਦਾ ਵਾਲਟ ਮਿਲਿਆ ਹੈ।\nਨਵਾਂ ਵਾਲਟ ਬਣਾਉਣ ਲਈ ਪਿੱਛੇ ਜਾ ਕੇ ਵੱਖਰਾ ਫੋਲਡਰ ਚੁਣੋ।"; +"addVault.createNewVault.password.enterPassword.header" = "ਨਵਾਂ ਪਾਸਵਰਡ ਦਿਓ।"; +"addVault.createNewVault.password.confirmPassword.header" = "ਨਵੇਂ ਪਾਸਵਰਡ ਨੂੰ ਤਸਦੀਕ ਕਰੋ।"; +"addVault.createNewVault.password.confirmPassword.alert.title" = "ਪਾਸਵਰਡ ਤਸਦੀਕ ਕਰਨਾ ਹੈ?"; +"addVault.createNewVault.password.confirmPassword.alert.message" = "ਜ਼ਰੂਰੀ: ਜੇ ਤੁਸੀਂ ਆਪਣਾ ਪਾਸਵਰਡ ਭੁੱਲ ਗਏ ਤਾਂ ਤੁਹਾਡੇ ਡਾਟੇ ਨੂੰ ਬਹਾਲ ਕਰਨ ਦਾ ਕੋਈ ਵੀ ਢੰਗ ਨਹੀਂ ਹੈ।"; +"addVault.createNewVault.password.error.emptyPassword" = "ਪਾਸਵਰਡ ਖਾਲੀ ਨਹੀਂ ਹੋ ਸਕਦਾ ਹੈ।"; +"addVault.createNewVault.password.error.nonMatchingPasswords" = "ਪਾਸਵਰਡ ਮਿਲਦਾ ਨਹੀਂ ਹੈ।"; +"addVault.createNewVault.password.error.tooShortPassword" = "ਪਾਸਵਰਡ ਘੱਟੋ-ਘੱਟ 8 ਅੱਖਰਾਂ ਦਾ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ।"; +"addVault.createNewVault.progress" = "ਵਾਲਟ ਬਣਾਇਆ ਜਾ ਰਿਹਾ ਹੈ…"; "addVault.openExistingVault.title" = "ਮੌਜੂਦਾ ਵਾਲਟ ਖੋਲ੍ਹੋ"; +"addVault.openExistingVault.chooseCloud.header" = "ਵਾਲਟ ਕਿੱਥੇ ਮੌਜੂਦ ਹੈ?"; +"addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator ਨੂੰ \"%@\" ਵਾਲਟ ਖੋਜਿਆ ਗਿਆ ਹੈ।\nਕੀ ਤੁਸੀ ਇਹ ਵਾਲਟ ਜੋੜਨਾ ਚਾਹੁੰਦੇ ਹੋ?"; +"addVault.openExistingVault.detectedMasterkey.add" = "ਇਹ ਵਾਲਟ ਜੋੜੋ"; +"addVault.openExistingVault.password.footer" = "\"%@\" ਲਈ ਪਾਸਵਰਡ ਦਿਓ।"; +"addVault.openExistingVault.progress" = "…ਵਾਲਟ ਜੋੜਿਆ ਜਾ ਰਿਹਾ ਹੈ"; +"addVault.success.info" = "\"%@\" ਵਾਲਟ ਨੂੰ ਕਾਮਯਾਬੀ ਨਾਲ ਜੋੜਿਆ ਗਿਆ ਹੈ।\nਇਸ ਵਾਲਟ ਨੂੰ Files ਐਪ ਰਾਹੀਂ ਵਰਤੋਂ।"; +"addVault.success.footer" = "ਜੇ ਤੁਸੀਂ ਹਾਲੇ ਨਹੀਂ ਕੀਤਾ ਤਾਂ Files ਐਪ ਵਿੱਚ Cryptomator ਨੂੰ ਸਮਰੱਥ ਕਰੋ।"; + +"biometryType.faceID" = "ਫੇਸ ID"; +"biometryType.touchID" = "ਟੱਚ ID"; + +"changePassword.error.invalidOldPassword" = "ਮੌਜੂਦਾ ਪਾਸਵਰਡ ਗਲਤ ਹੈ। ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ।"; +"changePassword.header.currentPassword.title" = "ਮੌਜੂਦਾ ਪਾਸਵਰਡ ਦਿਓ।"; +"changePassword.header.newPassword.title" = "ਨਵਾਂ ਪਾਸਵਰਡ ਦਿਓ।"; +"changePassword.header.newPasswordConfirmation.title" = "ਨਵੇਂ ਪਾਸਵਰਡ ਨੂੰ ਤਸਦੀਕ ਕਰੋ।"; +"changePassword.progress" = "…ਪਾਸਵਰਡ ਬਦਲਿਆ ਜਾ ਰਿਹਾ ਹੈ"; + +"chooseFolder.emptyFolder.footer" = "ਫੋਲਡਰ ਖਾਲੀ ਹੈ"; +"chooseFolder.createNewFolder.header.title" = "ਫੋਲਡਰ ਲਈ ਨਾਂ ਚੁਣੋ।"; +"chooseFolder.createNewFolder.cells.name" = "ਫੋਲਡਰ ਦਾ ਨਾਂ"; +"chooseFolder.createNewFolder.error.emptyFolderName" = "ਫ਼ੋਲਡਰ ਨਾਂ ਖਾਲੀ ਨਹੀਂ ਹੋ ਸਕਦਾ ਹੈ।"; +"chooseFolder.createNewFolder.progress" = "…ਫੋਲਡਰ ਬਣਾਇਆ ਜਾ ਰਿਹਾ ਹੈ"; + +"cloudProvider.error.itemNotFound" = "\"%@\" ਲੱਭਿਆ ਨਹੀਂ ਜਾ ਸਕਿਆ।"; +"cloudProvider.error.itemAlreadyExists" = "\"%@\" ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ।"; +"cloudProvider.error.itemTypeMismatch" = "\"%@\" ਅਣਪਛਾਤੀ ਆਈਟਮ ਕਿਸਮ ਹੈ। "; +"cloudProvider.error.parentFolderDoesNotExist" = "ਮੁੱਢਲਾ ਫੋਲਡਰ \"%@\" ਮੌਜੂਦ ਨਹੀਂ ਹੈ।"; +"cloudProvider.error.quotaInsufficient" = "ਤੁਹਾਡੇ ਕੋਲ ਨਾ-ਕਾਫ਼ੀ ਖਾਲੀ ਥਾਂ ਹੈ।"; +"cloudProvider.error.noInternetConnection" = "ਇਸ ਕਾਰਵਾਈ ਲਈ ਇੰਟਰਨੈੱਟ ਕਨੈਕਸ਼ਨ ਦੀ ਲੋੜ ਹੈ।"; + +"cloudProviderType.localFileSystem" = "ਹੋਰ ਫਾਇਲ ਪੂਰਕ"; + +"fileProvider.onboarding.title" = "ਜੀ ਆਇਆਂ ਨੂੰ"; +"fileProvider.onboarding.info" = "ਆਪਣੀਆਂ ਫਾਇਲਾਂ ਨੂੰ ਸੁਰੱਖਿਅਤ ਬਣਾਉਣ ਲਈ Cryptomator ਚੁਣਨ ਵਾਸਤੇ ਧੰਨਵਾਦ ਹੈ। ਸ਼ੁਰੂ ਕਰਨ ਲਈ ਮੁੱਖ ਐਪ ਉੱਤੇ ਜਾਓ ਅਤੇ ਵਾਲਟ ਜੋੜੋ।"; +"fileProvider.onboarding.button.openCryptomator" = "Cryptomator ਖੋਲ੍ਹੋ"; +"fileProvider.error.biometricalAuthCanceled.title" = "ਅਣ-ਲਾਕ ਕਰਨ ਰੱਦ ਕੀਤਾ"; +"fileProvider.error.biometricalAuthCanceled.message" = "%@ ਰਾਹੀਂ ਅਣ-ਲਾਕ ਕਰਨਾ ਕਾਮਯਾਬ ਨਹੀਂ ਹੈ। ਬਾਅਦ ਵਿੱਚ ਫੇਰ ਕੋਸ਼ਿਸ ਕਰੋ।"; +"fileProvider.error.biometricalAuthWrongPassword.title" = "ਗ਼ਲਤ ਪਾਸਵਰਡ"; +"fileProvider.error.biometricalAuthWrongPassword.message" = "%@ ਲਈ ਸੰਭਾਲਿਆ ਗਿਆ ਪਾਸਵਰਡ ਗਲਤ ਹੈ। ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਅਤੇ %@ ਨੂੰ ਮੁੜ-ਸਮਰੱਥ ਕਰਨ ਵਾਸਤੇ ਆਪਣਾ ਪਾਸਵਰਡ ਦਿਓ।"; +"fileProvider.error.defaultLock.title" = "ਅਣ-ਲਾਕ ਕਰਨ ਦੀ ਲੋੜ ਹੈ"; +"fileProvider.error.defaultLock.message" = "ਤੁਹਾਡੇ ਵਾਲਟ ਦੀ ਸਮੱਗਰੀ ਨੂੰ ਵਰਤਣ ਅਤੇ ਵੇਖਣ ਲਈ ਇਸ ਨੂੰ ਅਣ-ਲਾਕ ਕਰਨ ਦੀ ਲੋੜ ਹੈ।"; "fileProvider.error.unlockButton" = "ਅਣ-ਲਾਕ ਕਰੋ"; +"fileProvider.clearFileFromCache.title" = "ਕੈਸ਼ ਤੋਂ ਫਾਇਲ ਮਿਟਾਓ"; +"fileProvider.clearFileFromCache.message" = "ਇਹ ਸਿਰਫ਼ ਤੁਹਾਡੇ ਡਿਵਾਈਸ ਤੋਂ ਲੋਕਲ ਫਾਇਲ ਹੀ ਹਟਾਉਂਦਾ ਹੈ ਅਤੇ ਕਲਾਉਡ ਤੋਂ ਫਾਇਲ ਨੂੰ ਨਹੀਂ ਹਟਾਉਂਦਾ ਹੈ।"; +"fileProvider.fileImporting.error.missingPremium" = "ਤੁਹਾਡੇ ਵਾਲਟਾਂ ਲਈ ਲਿਖਣ ਪਹੁੰਚਣ ਲਈ ਵਾਸਤੇ Cryptomator ਐਪ ਦਾ ਪੂਰਾ ਵਰਜ਼ਨ ਅਣ-ਲਾਕ ਕਰੋ।"; +"fileProvider.uploadProgress.connecting" = "…ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"; +"fileProvider.uploadProgress.title" = "…ਅੱਪਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"; +"getFolderIntent.error.noVaultSelected" = "ਕੋਈ ਵਾਲਟ ਨਹੀਂ ਚੁਣਿਆ ਹੈ।"; + +"keepUnlocked.alert.title" = "ਵਾਲਟ ਲਾਕ ਕਰਨਾ ਹੈ?"; +"keepUnlocked.alert.confirm" = "ਤਸਦੀਕ ਅਤੇ ਹੁਣੇ ਲਾਕ ਕਰੋ"; +"keepUnlockedDuration.auto.shortDisplayName" = "ਆਟੋ"; +"localFileSystemAuthentication.createNewVault.button" = "ਸਟੋਰੇਜ਼ ਦਾ ਟਿਕਾਣਾ ਚੁਣੋ"; +"localFileSystemAuthentication.createNewVault.error.detectedExistingVault" = "ਇਸ ਟਿਕਾਣੇ ਉੱਤੇ ਵਾਲਟ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ। ਵੱਖਰੇ ਸਟੋਰੇਜ਼ ਟਿਕਾਣੇ ਨੂੰ ਚੁਣ ਕੇ ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ।"; +"localFileSystemAuthentication.openExistingVault.header" = "ਅਗਲੀ ਸਕਰੀਨ ਉੱਤੇ ਆਪਣੇ ਮੌਜੂਦਾ ਵਾਲਟ ਦਾ ਫੋਲਡਰ ਚੁਣੋ।"; +"localFileSystemAuthentication.openExistingVault.button" = "ਵਾਲਟ ਫੋਲਡਰ ਚੁਣੋ"; +"localFileSystemAuthentication.openExistingVault.error.noVaultFound" = "ਚੁਣਿਆ ਫੋਲਡਰ ਵਾਲਟ ਨਹੀਂ ਹੈ। ਵੱਖਰੇ ਫੋਲਡਰ ਨਾਲ ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ।"; + +"onboarding.title" = "ਜੀ ਆਇਆਂ ਨੂੰ"; +"onboarding.info" = "ਆਪਣੀਆਂ ਫਾਇਲਾਂ ਨੂੰ ਸੁਰੱਖਿਅਤ ਕਰਨ ਵਾਸਤੇ Cryptomator ਚੁਣਨ ਲਈ ਧੰਨਵਾਦ ਹੈ।\n\nCryptomator ਨਾਲ ਤੁਹਾਡੇ ਡਾਟੇ ਦੀ ਕੁੰਜੀ ਤੁਹਾਡੇ ਹੱਥ ਵਿੱਚ ਹੈ। Cryptomator ਤੁਹਾਡੇ ਡਾਟੇ ਨੂੰ ਫ਼ੌਰਨ ਅਤੇ ਸੌਖੀ ਤਰ੍ਹਾਂ ਇੰਕ੍ਰਿਪਟ ਕਰਦਾ ਹੈ।\n\nਇਹ ਐਪ ਪੂਰੀ ਤਰ੍ਹਾਂ Files ਐਪ ਨਾਲ ਜੁੜੀ ਹੋਈ ਹੈ। ਬਾਅਦ ਆਪਣੇ ਵਾਲਟਾਂ ਨੂੰ ਵਰਤਣ Files ਐਪ ਵਿੱਚ Cryptomator ਨੂੰ ਸਮਰੱਥ ਕਰਨਾ ਨਾ ਭੁੱਲੋ।"; +"onboarding.button.continue" = "ਜਾਰੀ ਰੱਖੋ"; + +"purchase.beginFreeTrial.alert.title" = "ਅਜ਼ਮਾਇਸ਼ ਅਣ-ਲਾਕ ਹੈ"; +"purchase.expiredTrial" = "ਤੁਹਾਡੀ ਅਜ਼ਮਾਇਜ਼ ਦੀ ਮਿਆਦ ਪੁੱਗੀ ਹੈ।"; +"purchase.footer.privacyPolicy" = "ਪਰਦੇਦਾਰੀ ਨੀਤੀ"; +"purchase.footer.termsOfUse" = "ਵਰਤਣ ਦੀਆਂ ਸ਼ਰਤਾਂ"; +"purchase.header.feature.familySharing" = "ਪਰਿਵਾਰ ਲਈ ਸਾਂਝਾ"; +"purchase.header.feature.openSource" = "ਓਪਨ-ਸਰੋਤ ਵਿਕਾਸ"; +"purchase.header.feature.writeAccess" = "ਤੁਹਾਡੇ ਵਾਲਟਾਂ ਲਈ ਲਿਖਣ ਪਹੁੰਚ"; +"purchase.product.donateAndUpgrade" = "ਦਾਨ ਦਿਓ ਤੇ ਅੱਪਗਰੇਡ ਕਰੋ"; +"purchase.product.freeUpgrade" = "ਮੁਫ਼ਤ ਅੱਪਗਰੇਡ"; +"purchase.product.lifetimeLicense" = "ਜ਼ਿੰਗਦੀ ਭਰ ਲਈ ਲਸੰਸ"; +"purchase.product.lifetimeLicense.duration" = "ਇੱਕ-ਵਾਰ"; +"purchase.product.pricing.free" = "ਮੁਫ਼ਤ"; +"purchase.product.trial" = "30-ਦਿਨਾਂ ਲਈ ਅਜ਼ਮਾਇਸ਼"; +"purchase.product.trial.expirationDate" = "ਮਿਆਦ: %@"; +"purchase.product.trial.duration" = "30 ਦਿਨਾਂ ਲਈ"; +"purchase.product.yearlySubscription" = "ਸਾਲਨਾ ਮੈਂਬਰੀ"; +"purchase.product.yearlySubscription.duration" = "ਸਾਲਨਾ"; +"purchase.readOnlyMode.alert.title" = "ਕੇਵਲ ਪੜ੍ਹਨ ਲਈ ਢੰਗ"; +"purchase.retry.button" = "ਮੁੜ-ਕੋਸ਼ਿਸ਼"; +"purchase.title" = "ਪੂਰਾ ਵਰਜ਼ਨ ਅਣ-ਲਾਕ ਕਰੋ"; +"purchase.unlockedFullVersion.title" = "ਤੁਹਾਡਾ ਧੰਨਵਾਦ"; + +"settings.title" = "ਸੈਟਿੰਗਾਂ"; +"settings.aboutCryptomator" = "Cryptomator ਖੋਲ੍ਹੋ"; +"settings.aboutCryptomator.title" = "ਵਰਜ਼ਨ %@ (%@)"; +"settings.cacheSize" = "ਕੈਸ਼ ਆਕਾਰ"; +"settings.clearCache" = "ਕੈਸ਼ ਮਿਟਾਓ"; +"settings.cloudServices" = "ਕਲਾਉਡ ਸੇਵਾਵਾਂ"; +"settings.contact" = "ਸੰਪਰਕ ਕਰੋ"; +"settings.debugMode" = "ਡੀਬੱਗ ਢੰਗ"; +"settings.manageSubscriptions" = "ਮੈਂਬਰੀ ਦਾ ਇੰਤਜ਼ਾਮ ਕਰੋ"; +"settings.rateApp" = "ਐਪ ਨੂੰ ਦਰਜਾ ਦਿਓ"; +"settings.sendLogFile" = "ਲਾਗ ਫਾਇਲ ਭੇਝੋ"; +"settings.shortcutsGuide" = "ਸ਼ਾਰਟਕੱਟ ਗਾਈਡ"; +"settings.unlockFullVersion" = "ਪੂਰਾ ਵਰਜ਼ਨ ਅਣ-ਲਾਕ ਕਰੋ"; + +"s3Authentication.displayName" = "ਦਿਖਾਉਣ ਲਈ ਨਾਂ"; +"s3Authentication.accessKey" = "ਪਹੁੰਚ ਕੁੰਜੀ"; +"s3Authentication.secretKey" = "ਗੁਪਤ ਕੁੰਜੀ"; +"s3Authentication.existingBucket" = "ਮੌਜੂਦਾ ਡੱਬਾ"; +"s3Authentication.endpoint" = "ਐਂਡ-ਪੁਆਇੰਟ"; +"s3Authentication.region" = "ਖੇਤਰ"; +"s3Authentication.error.invalidCredentials" = "ਗਲਤ ਸਨਦਾਂ।"; + +"trialStatus.active" = "ਸਰਗਰਮ"; +"trialStatus.expired" = "ਮਿਆਦ ਪੁੱਗ ਗਈ"; "unlockVault.button.unlock" = "ਅਣ-ਲਾਕ ਕਰੋ"; +"unlockVault.button.unlockVia" = "%@ ਰਾਹੀਂ ਅਣ-ਲਾਕ"; +"unlockVault.password.footer" = "\"%@\" ਲਈ ਪਾਸਵਰਡ ਦਿਓ।"; +"unlockVault.enableBiometricalUnlock.switch" = "%@ ਸਮਰੱਥ ਕਰੋ"; +"unlockVault.enableBiometricalUnlock.footer" = "ਆਪਣੇ ਪਾਸਵਰਡ ਨਾਲ ਆਪਣੇ ਵਾਲਟ ਨੂੰ ਅਣ-ਲਾਕ ਕਰਨ ਦੀ ਬਜਾਏ ਇਸ ਨੂੰ %@ ਰਾਹੀਂ ਅਣ-ਲਾਕ ਕਰ ਸਕਦੇ ਹੋ।"; +"unlockVault.evaluatePolicy.reason" = "ਆਪਣੇ ਵਾਲਟ ਨੂੰ ਅਣ-ਲਾਕ ਕਰੋ"; +"unlockVault.progress" = "…ਅਣ-ਲਾਕ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"; +"untrustedTLSCertificate.add" = "ਭਰੋਸਾ ਕਰੋ"; +"untrustedTLSCertificate.dismiss" = "ਭਰੋਸਾ ਨਾ ਕਰੋ"; + +"upgrade.title" = "ਅੱਪਗਰੇਡ ਪੇਸ਼ਕਸ਼"; +"upgrade.notEligible.alert.title" = "ਅੱਪਗਰੇਡ ਫੇਲ੍ਹ ਹੈ"; +"upgrade.info" = "ਵਰਜ਼ਨ ਇੱਕ ਤੋਂ ਲੈ ਕੇ Cryptomator ਉੱਤੇ ਭਰੋਸਾ ਕਰਨ ਲਈ ਧੰਨਵਾਦ ਹੈ। ਤੁਹਾਡੇ ਵਰਗੇ ਸਮਰਪਿਤ ਵਰਤੋਂਕਾਰ ਮੁਫ਼ਤ ਅੱਪਗਰੇਡ ਲਈ ਪਾਤਰ ਹਨ।"; + +"urlSession.error.httpError.401" = "ਗਲਤ ਵਰਤੋਂਕਾਰ ਨਾਂ ਅਤੇ/ਜਾਂ ਪਾਸਵਰਡ ਹੈ।"; "vaultDetail.button.changeVaultPassword" = "ਪਾਸਵਰਡ ਬਦਲੋ"; +"vaultDetail.button.lock" = "ਹੁਣੇ ਲਾਕ ਕਰੋ"; +"vaultDetail.button.moveVault" = "ਭੇਜੋ"; +"vaultDetail.button.removeVault" = "ਵਾਲਟ ਸੂਚੀ ਵਿੱਚੋਂ ਹਟਾਓ"; +"vaultDetail.button.renameVault" = "ਨਾਂ ਬਦਲੋ"; +"vaultDetail.moveVault.progress" = "…ਭੇਜਿਆ ਜਾ ਰਿਹਾ ਹੈ"; +"vaultDetail.renameVault.progress" = "…ਨਾਂ ਬਦਲਿਆ ਜਾ ਰਿਹਾ ਹੈ"; + +"vaultList.header.title" = "ਵਾਲਟ"; +"vaultList.emptyList.message" = "ਵਾਲਟ ਜੋੜਨ ਲਈ ਇੱਥੇ ਛੂਹੋ"; +"vaultList.remove.alert.title" = "ਵਾਲਟ ਹਟਾਉਣਾ ਹੈ?"; + +"Retry Upload" = "ਅੱਪਲੋਡ ਦੀ ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ"; +"Clear from Cache" = "ਕੈਸ਼ ਤੋਂ ਮਿਟਾਓ"; diff --git a/SharedResources/pl.lproj/Localizable.strings b/SharedResources/pl.lproj/Localizable.strings index f071d69f6..a99e191e2 100644 --- a/SharedResources/pl.lproj/Localizable.strings +++ b/SharedResources/pl.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Włącz"; "common.button.next" = "Dalej"; "common.button.ok" = "OK"; +"common.button.refresh" = "Odśwież"; +"common.button.register" = "Zarejestruj"; "common.button.remove" = "Usuń"; "common.button.retry" = "Ponów"; "common.button.signOut" = "Wyloguj"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Gdzie znajduje się sejf?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator wykrył sejf \"%@\".\nCzy chcesz dodać ten sejf?"; "addVault.openExistingVault.detectedMasterkey.add" = "Dodaj ten sejf"; +"addVault.openExistingVault.downloadVault.progress" = "Pobieranie sejfu…"; "addVault.openExistingVault.password.footer" = "Wprowadź hasło dla \"%@\"."; "addVault.openExistingVault.progress" = "Dodawanie sejfu…"; "addVault.success.info" = "Dodano sejf \"%@\".\nUzyskaj dostęp do tego sejfu poprzez aplikację Pliki."; @@ -110,6 +113,18 @@ "getFolderIntent.error.missingPath" = "Nie podano ścieżki. Proszę podać prawidłową ścieżkę do folderu."; "getFolderIntent.error.noVaultSelected" = "Nie wybrano sejfu."; + +"hubAuthentication.title" = "Hub sejfów"; +"hubAuthentication.accessNotGranted" = "Twoje urządzenie nie zostało jeszcze upoważnione do dostępu do tego sejfu. Poproś właściciela sejfu o autoryzację."; +"hubAuthentication.licenseExceeded" = "Twoja instancja Hub ma nieprawidłową licencję. Poproś administratora Hub o uaktualnienie lub odnowienie licencji."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nazwa urządzenia"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Wygląda że jest to pierwszy dostęp do Huba z tego urządzenia. Aby zidentyfikować go w celu autoryzacji dostępu, musisz nazwać to urządzenie."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Twój Klucz Konta jest wymagany do zalogowania się z nowych aplikacji lub przeglądarek. Można go znaleźć w twoim profilu."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Rejestracja urządzenia powiodła się"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Aby dostać się do sejfu, Twoje urządzenie musi być autoryzowane przez właściciela sejfu."; +"hubAuthentication.requireAccountInit.alert.title" = "Wymagane działanie"; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Przejdź do profilu"; + "intents.saveFile.missingFile" = "Wskazany plik nie jest prawidłowy."; "intents.saveFile.invalidFolder" = "Wskazany folder jest niewłaściwy."; "intents.saveFile.missingTemporaryFolder" = "Nie udało się utworzyć katalogu tymczasowego."; diff --git a/SharedResources/pt-BR.lproj/Localizable.strings b/SharedResources/pt-BR.lproj/Localizable.strings index c2800f8ae..34160fc48 100644 --- a/SharedResources/pt-BR.lproj/Localizable.strings +++ b/SharedResources/pt-BR.lproj/Localizable.strings @@ -15,12 +15,14 @@ "common.button.confirm" = "Confirmar"; "common.button.create" = "Criar"; "common.button.createFolder" = "Criar pasta"; -"common.button.done" = "Pronto"; +"common.button.done" = "Concluído"; "common.button.download" = "Baixar"; "common.button.edit" = "Editar"; "common.button.enable" = "Habilitar"; "common.button.next" = "Próximo"; "common.button.ok" = "Ok"; +"common.button.refresh" = "Atualizar"; +"common.button.register" = "Registrar"; "common.button.remove" = "Remover"; "common.button.retry" = "Tentar Novamente"; "common.button.signOut" = "Finalizar sessão"; @@ -43,7 +45,7 @@ "addVault.createNewVault.setVaultName.header.title" = "Escolha um nome para o cofre."; "addVault.createNewVault.setVaultName.cells.name" = "Nome do Cofre"; "addVault.createNewVault.setVaultName.error.emptyVaultName" = "O nome do cofre não pode estar vazio."; -"addVault.createNewVault.chooseCloud.header" = "Onde o Cryptomator deve armazenar os arquivos encriptados do seu cofre?"; +"addVault.createNewVault.chooseCloud.header" = "Onde o Cryptomator deve armazenar os arquivos criptografados do seu cofre?"; "addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@já existe neste local. Escolha um nome ou local de um cofre diferente."; "addVault.createNewVault.detectedMasterkey.text" = "O Cryptomator detectou um cofre existente neste local.\nPara criar um novo cofre, por favor, volte e escolha uma pasta diferente."; "addVault.createNewVault.password.enterPassword.header" = "Digite uma nova senha."; @@ -54,17 +56,18 @@ "addVault.createNewVault.password.error.nonMatchingPasswords" = "As senhas não coincidem."; "addVault.createNewVault.password.error.tooShortPassword" = "A senha deve conter pelo menos 8 caracteres."; "addVault.createNewVault.progress" = "Criando Cofre…"; -"addVault.openExistingVault.title" = "Abrir Cofre"; +"addVault.openExistingVault.title" = "Abrir Cofre Existente"; "addVault.openExistingVault.chooseCloud.header" = "Onde o cofre está localizado?"; "addVault.openExistingVault.detectedMasterkey.text" = "O Cryptomator detectou o cofre \"%@\".\nVocê gostaria de adicionar este cofre?"; "addVault.openExistingVault.detectedMasterkey.add" = "Adicionar Este Cofre"; +"addVault.openExistingVault.downloadVault.progress" = "Baixando o cofre…"; "addVault.openExistingVault.password.footer" = "Digite a senha para \"%@\"."; "addVault.openExistingVault.progress" = "Adicionando Cofre…"; -"addVault.success.info" = "Cofre \"%@\" adicionado com sucesso.\nAcesse-o através do aplicativo Arquivos."; +"addVault.success.info" = "Cofre \"%@\" adicionado com sucesso.\nAcesse-o pelo aplicativo Arquivos."; "addVault.success.footer" = "Se você ainda não o fez, ative o Cryptomator no aplicativo Arquivos."; "biometryType.faceID" = "Face ID"; -"biometryType.touchID" = "ID do toque"; +"biometryType.touchID" = "Touch ID"; "changePassword.error.invalidOldPassword" = "Senha atual incorreta. Favor tentar novamente."; "changePassword.header.currentPassword.title" = "Digite a senha atual."; @@ -110,6 +113,15 @@ "getFolderIntent.error.missingPath" = "Nenhum caminho foi fornecido. Por favor, forneça um caminho válido para o qual a pasta deve ser retornada."; "getFolderIntent.error.noVaultSelected" = "Nenhum cofre foi selecionado."; + +"hubAuthentication.title" = "Cofre do Hub"; +"hubAuthentication.accessNotGranted" = "Seu dispositivo ainda não foi autorizado a acessar este cofre. Peça ao proprietário do cofre para autorizá-lo."; +"hubAuthentication.licenseExceeded" = "Sua instância do Cryptomator Hub possui uma licença inválida. Por favor, informe um administrador do Hub para atualizar ou renovar a licença."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nome do dispositivo"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Parece ser o primeiro acesso do Hub a partir deste dispositivo. Para identificá-lo para autorização de acesso, você precisa nomear este dispositivo."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Dispositivo registrado com sucesso"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Para acessar o cofre, seu dispositivo precisa ser autorizado pelo proprietário do cofre."; + "intents.saveFile.missingFile" = "O arquivo fornecido não é válido."; "intents.saveFile.invalidFolder" = "A pasta fornecida não é válida."; "intents.saveFile.missingTemporaryFolder" = "Falha ao criar uma pasta temporária."; @@ -148,7 +160,7 @@ "purchase.expiredTrial" = "Seu período de teste expirou."; "purchase.footer.privacyPolicy" = "Política de Privacidade"; "purchase.footer.termsOfUse" = "Termos de Uso"; -"purchase.header.feature.familySharing" = "Compartilhamento de família"; +"purchase.header.feature.familySharing" = "Compartilhamento familiar"; "purchase.header.feature.openSource" = "Desenvolvimento de código aberto"; "purchase.header.feature.writeAccess" = "Acesso de escrita aos seus cofres"; "purchase.product.donateAndUpgrade" = "Doe e faça um “upgrade”"; @@ -207,13 +219,13 @@ "snapshots.main.vault4" = "/Viagem pra Califórnia"; "s3Authentication.displayName" = "Nome de Exibição"; -"s3Authentication.accessKey" = "Chave de acesso"; +"s3Authentication.accessKey" = "Chave de Acesso"; "s3Authentication.secretKey" = "Chave Secreta"; -"s3Authentication.existingBucket" = "Bucket do nome de domínio na URL existente"; -"s3Authentication.endpoint" = "URL para autenticação no Amazon S3 — “Endpoint”"; +"s3Authentication.existingBucket" = "Bucket existente"; +"s3Authentication.endpoint" = "Endpoint"; "s3Authentication.region" = "Região"; "s3Authentication.error.invalidCredentials" = "Credenciais inválidas."; -"s3Authentication.error.invalidEndpoint" = "O Endpoint — “link” — fornecido não coincide com o formato de uma URL válida no Amazon S3."; +"s3Authentication.error.invalidEndpoint" = "O endpoint fornecido não coincide com o formato de uma URL."; "trialStatus.active" = "Ativo"; "trialStatus.expired" = "Expirado"; @@ -279,7 +291,7 @@ Para mover o seu cofre, por favor, volte e escolha uma pasta diferente."; "vaultProviderFactory.error.unsupportedVaultVersion" = "A versão do Cofre %ld não é suportada. Este cofre foi criado com uma versão mais antiga ou mais recente do Cryptomator."; "webDAVAuthentication.httpConnection.alert.title" = "Usar HTTPS?"; -"webDAVAuthentication.httpConnection.alert.message" = "O uso de HTTP é inseguro. Recomendamos a utilização de HTTPS. Se você souber os riscos, você pode continuar com o HTTP."; +"webDAVAuthentication.httpConnection.alert.message" = "O uso de HTTP é inseguro. Recomendamos utilizar HTTPS. Se você conhecer os riscos, pode continuar com HTTP."; "webDAVAuthentication.httpConnection.change" = "Mudar para HTTPS"; "webDAVAuthentication.httpConnection.continue" = "Manter HTTP"; diff --git a/SharedResources/pt.lproj/Localizable.strings b/SharedResources/pt.lproj/Localizable.strings index acb66d590..998d175f9 100644 --- a/SharedResources/pt.lproj/Localizable.strings +++ b/SharedResources/pt.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Ativar"; "common.button.next" = "Seguinte"; "common.button.ok" = "OK"; +"common.button.refresh" = "Atualizar"; +"common.button.register" = "Registo"; "common.button.remove" = "Remover"; "common.button.retry" = "Tentar de novo"; "common.button.signOut" = "Terminar Sessão"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Onde está o cofre localizado?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator detetou o cofre \"%@\".\nGostaria de adicionar este cofre?"; "addVault.openExistingVault.detectedMasterkey.add" = "Adicionar Este Cofre"; +"addVault.openExistingVault.downloadVault.progress" = "Descarregando o cofre…"; "addVault.openExistingVault.password.footer" = "Insira a palavra-passe para \"%@\"."; "addVault.openExistingVault.progress" = "A Adicionar Cofre…"; "addVault.success.info" = "Cofre \"%@\" adicionado com sucesso.\nAccesse este cofre através da aplicação Ficherios."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Trajeto não foi fornecido. Por favor, forneça um trajeto válido para o qual a pasta deve ser retornada."; "getFolderIntent.error.noVaultSelected" = "Nenhum cofre selecionado."; + +"hubAuthentication.title" = "Cofre do Hub"; +"hubAuthentication.accessNotGranted" = "O seu dispositivo ainda não foi autorizado a aceder a este cofre. Peça ao proprietário do cofre para o autorizar."; +"hubAuthentication.licenseExceeded" = "A entidade do seu Cryptomator Hub tem uma licença inválida. Por favor, informe o administrador do Hub para atualizar ou renovar a licença."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nome do dispositivo"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Parece ser o primeiro acesso ao Hub a partir deste dispositivo. Para identificá-lo para autorização de acesso, é preciso dar um nome a este dispositivo."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "A sua chave de conta é necessária para iniciar sessão em novos aplicativos ou navegadores. Ela pode ser encontrada no seu perfil."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Dispositivo registado com sucesso"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Para aceder ao cofre, o seu dispositivo precisa de ser autorizado pelo proprietário do cofre."; +"hubAuthentication.requireAccountInit.alert.title" = "Ação necessária"; +"hubAuthentication.requireAccountInit.alert.message" = "Para continuar, por favor conclua as etapas exigidas no seu perfil de utilizador no Hub."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Vá para o Perfil"; + "intents.saveFile.missingFile" = "O ficheiro especificado não é válido."; "intents.saveFile.invalidFolder" = "A pasta especificada não é válida."; "intents.saveFile.missingTemporaryFolder" = "Não foi possível criar a pasta temporária."; diff --git a/SharedResources/ro.lproj/Localizable.strings b/SharedResources/ro.lproj/Localizable.strings index 250210c77..96bba5938 100644 --- a/SharedResources/ro.lproj/Localizable.strings +++ b/SharedResources/ro.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "Activează"; "common.button.next" = "Următor"; "common.button.ok" = "OK"; +"common.button.refresh" = "Împrospătează"; "common.button.remove" = "Șterge"; "common.button.retry" = "Încercați din nou"; "common.button.signOut" = "Deconectare"; @@ -110,6 +111,11 @@ "getFolderIntent.error.missingPath" = "Nu a fost furnizată nicio locație. Vă rugăm să alegeți o cale validă pentru care un dosar trebuie returnat."; "getFolderIntent.error.noVaultSelected" = "Nici un seif nu a fost ales."; +"hubAuthentication.accessNotGranted" = "Dispozitivul dvs. nu a fost autorizat să acceseze acest seif. Solicitați proprietarului seifului să va autorizeze accesul."; +"hubAuthentication.licenseExceeded" = "Instanța Hub are o licență invalidă. Vă rugăm să informați un administrator Hub să actualizeze sau să reînnoiască licența."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Numele dispozitivului"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Pentru a accesa acest seif, dispozitivul dvs. trebuie să fie autorizat de proprietarul seifului."; + "intents.saveFile.missingFile" = "Fișierul ales nu este valid."; "intents.saveFile.invalidFolder" = "Dosarul ales nu este valid."; "intents.saveFile.missingTemporaryFolder" = "Nu s-a putut crea dosarului temporar."; @@ -265,7 +271,7 @@ "vaultDetail.moveVault.detectedMasterkey.text" = "Cryptomator a detectat un seif în această locație.\nPentru a crea un nou seif, vă rugăm să alegeți un folder diferit."; "vaultDetail.moveVault.progress" = "Se mută…"; "vaultDetail.removeVault.footer" = "Acest lucru va elimina doar seiful din lista de seifuri și nu va șterge niciun fișier criptat."; -"vaultDetail.renameVault.progress" = "In proces de redenumire…"; +"vaultDetail.renameVault.progress" = "În proces de redenumire…"; "vaultDetail.unlocked.footer" = "Seiful dvs. este în prezent deblocat în aplicația fișiere."; "vaultDetail.unlockVault.footer" = "Introduceți parola pentru \"%@\" pentru a o stoca în iOS keychain și pentru a activa %@."; diff --git a/SharedResources/ru.lproj/Localizable.strings b/SharedResources/ru.lproj/Localizable.strings index da8c44010..ab803903f 100644 --- a/SharedResources/ru.lproj/Localizable.strings +++ b/SharedResources/ru.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Включить"; "common.button.next" = "Далее"; "common.button.ok" = "OK"; +"common.button.refresh" = "Обновить"; +"common.button.register" = "Регистрация"; "common.button.remove" = "Удалить"; "common.button.retry" = "Повторить"; "common.button.signOut" = "Выйти"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Где находится хранилище?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator обнаружил хранилище \"%@\".\nДобавить это хранилище?"; "addVault.openExistingVault.detectedMasterkey.add" = "Добавить это хранилище"; +"addVault.openExistingVault.downloadVault.progress" = "Загрузка хранилища…"; "addVault.openExistingVault.password.footer" = "Введите пароль для \"%@\"."; "addVault.openExistingVault.progress" = "Добавление хранилища…"; "addVault.success.info" = "Хранилище \"%@\" добавлено.\nДоступ к нему - через приложение \"Файлы\"."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Не указан путь. Укажите правильный путь, по которому должна быть возвращена папка."; "getFolderIntent.error.noVaultSelected" = "Не выбрано хранилище."; + +"hubAuthentication.title" = "Хаб-хранилище"; +"hubAuthentication.accessNotGranted" = "Ваше устройство ещё не авторизовано для доступа к этому хранилищу. Попросите владельца хранилища предоставить разрешение."; +"hubAuthentication.licenseExceeded" = "У вашего Cryptomator Hub неверная лицензия. Попросите Hub администратора обновить или продлить лицензию."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Имя устройства"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Похоже, это первый доступ к Hub с данного устройства. Чтобы идентифицировать его для предоставления доступа, нужно дать устройству имя."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Для входа в систему из новых приложений или браузеров требуется ключ вашего аккаунта. Он находится в вашем профиле."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Устройство успешно зарегистрировано"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Для доступа к хранилищу ваше устройство должно быть авторизовано владельцем хранилища."; +"hubAuthentication.requireAccountInit.alert.title" = "Требуется действие"; +"hubAuthentication.requireAccountInit.alert.message" = "Для продолжения выполните необходимые шаги в вашем профиле пользователя Hub."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Перейти в профиль"; + "intents.saveFile.missingFile" = "Выбран некорректный файл."; "intents.saveFile.invalidFolder" = "Выбрана некорректная папка."; "intents.saveFile.missingTemporaryFolder" = "Не удалось создать папку для временных файлов."; diff --git a/SharedResources/sk.lproj/Localizable.strings b/SharedResources/sk.lproj/Localizable.strings index c1c355473..c39c398e9 100644 --- a/SharedResources/sk.lproj/Localizable.strings +++ b/SharedResources/sk.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Povoliť"; "common.button.next" = "Ďalej"; "common.button.ok" = "OK"; +"common.button.refresh" = "Obnoviť"; +"common.button.register" = "Registrovať"; "common.button.remove" = "Odstrániť"; "common.button.retry" = "Skúsiť znovu"; "common.button.signOut" = "Odhlásiť"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Kde je trezor umiestnený?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator zdetekoval trezor \"%@\".\n Prejete si pridať tento trezor?"; "addVault.openExistingVault.detectedMasterkey.add" = "Pridať tento trezor"; +"addVault.openExistingVault.downloadVault.progress" = "Sťahovanie trezora…"; "addVault.openExistingVault.password.footer" = "Zadajte heslo pre \"%@\"."; "addVault.openExistingVault.progress" = "Pridávanie trezora…"; "addVault.success.info" = "Úspešne pridaný trezor \"%@\".\n Sprístupnený tento trezor prostredníctvom Súborovej aplikácie."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Nebola poskytnutá žiadna cesta. Prosím poskytnite platnú cestu pre adresár, ktorý by mal byť vrátený."; "getFolderIntent.error.noVaultSelected" = "Nebol zvolený žiaden trezor."; + +"hubAuthentication.title" = "Hub trezora"; +"hubAuthentication.accessNotGranted" = "Vaše zaradenie zatiaľ ešte nebolo autorizované pre pristúp tohto trezora. Požiadajte majiteľa trezora o autorizovanie."; +"hubAuthentication.licenseExceeded" = "Vaša inštancia Cryptomator Hub-u má neplatnú licenciu. Prosím informujte Hub administrátora pre aktualizáciu alebo obnovenie licencie."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Názov zariadenia"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Zdá sa, že ide o prvý prístup k Hub-u z tohto zariadenia. Z dôvodu identifikácie prístupovej autorizácie, je potrebné pomenovať toto zariadenie."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Váš kľúč účtu je vyžadovaný pre prihlásenie z nových aplikácii alebo prehliadačov. Môžete ho nájsť vo vašom profile."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Registrácia zariadenia úspešná"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Sprístupniť trezor, Vaše zariadenie musí byť autorizované vlastníkom trezora."; +"hubAuthentication.requireAccountInit.alert.title" = "Vyžaduje sa akcia"; +"hubAuthentication.requireAccountInit.alert.message" = "Pre pokračovanie, prosím splňte kroky požadované vo Vašom užívateľskom Hub profile."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Prejsť na profil"; + "intents.saveFile.missingFile" = "Poskytnutý súbor nie je platný."; "intents.saveFile.invalidFolder" = "Poskytnutý adresár nie je platný."; "intents.saveFile.missingTemporaryFolder" = "Nepodarilo sa vytvoriť dočasný adresár."; diff --git a/SharedResources/sl.lproj/Localizable.strings b/SharedResources/sl.lproj/Localizable.strings index 3b5442ffb..3510183c6 100644 --- a/SharedResources/sl.lproj/Localizable.strings +++ b/SharedResources/sl.lproj/Localizable.strings @@ -7,6 +7,7 @@ "common.button.edit" = "Uredi"; "common.button.next" = "Naslednji"; "common.button.ok" = "V redu"; +"common.button.refresh" = "Osveži"; "common.button.remove" = "Odstrani"; "common.button.retry" = "Poizkusi znova"; "common.cells.url" = "URL naslov"; diff --git a/SharedResources/sv.lproj/Localizable.strings b/SharedResources/sv.lproj/Localizable.strings index ab36b2083..d6b537e0e 100644 --- a/SharedResources/sv.lproj/Localizable.strings +++ b/SharedResources/sv.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Aktivera"; "common.button.next" = "Nästa"; "common.button.ok" = "OK"; +"common.button.refresh" = "Uppdatera"; +"common.button.register" = "Skapa konto"; "common.button.remove" = "Ta bort"; "common.button.retry" = "Försök igen"; "common.button.signOut" = "Logga ut"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Var ligger valvet?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator upptäckte valvet \"%@\".\nVill du lägga till detta valv?"; "addVault.openExistingVault.detectedMasterkey.add" = "Lägg till detta valv"; +"addVault.openExistingVault.downloadVault.progress" = "Laddar ner valv…"; "addVault.openExistingVault.password.footer" = "Ange lösenord för \"%@\"."; "addVault.openExistingVault.progress" = "Lägger till valvet…"; "addVault.success.info" = "Framgångsrikt lagt till valvet \"%@\".\nKom åt detta valv via Fil-appen."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Ingen sökväg angavs. Vänligen ange en giltig sökväg för vilken en mapp ska returneras."; "getFolderIntent.error.noVaultSelected" = "Inget valv har valts."; + +"hubAuthentication.title" = "Hubb valv"; +"hubAuthentication.accessNotGranted" = "Din enhet har ännu inte behörighet att komma åt detta valv. Be valvägaren att godkänna det."; +"hubAuthentication.licenseExceeded" = "Din Cryptomator Hub-instans har en ogiltig licens. Vänligen informera en Hub administratör för att uppgradera eller förnya licensen."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Enhetsnamn"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Detta verkar vara den första Hub-åtkomsten från den här enheten. För att identifiera den för åtkomstbehörighet, måste du namnge den här enheten."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Din kontonyckel krävs för att logga in från nya appar eller webbläsare. Den finns i din profil."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Enhetsregistrering lyckades"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "För att komma åt valvet måste din enhet godkännas av valvägaren."; +"hubAuthentication.requireAccountInit.alert.title" = "Åtgärd krävs"; +"hubAuthentication.requireAccountInit.alert.message" = "För att fortsätta, vänligen fyll i de steg som krävs i din Hub-användarprofil."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Gå till profil"; + "intents.saveFile.missingFile" = "Den angivna filen är inte giltig."; "intents.saveFile.invalidFolder" = "Den angivna mappen är inte giltig."; "intents.saveFile.missingTemporaryFolder" = "Det gick inte att skapa en temporär mapp."; diff --git a/SharedResources/sw-TZ.lproj/Localizable.strings b/SharedResources/sw-TZ.lproj/Localizable.strings index aa43f831b..c75a0bd30 100644 --- a/SharedResources/sw-TZ.lproj/Localizable.strings +++ b/SharedResources/sw-TZ.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "Wezesha"; "common.button.next" = "Nyingine"; "common.button.ok" = "Sawa"; +"common.button.refresh" = "Onesha upya"; "common.button.remove" = "Ondoa"; "common.button.retry" = "Jaribu tena"; "common.button.signOut" = "Ondoka"; @@ -110,6 +111,11 @@ "getFolderIntent.error.missingPath" = "Hakuna njia iliyotolewa. Tafadhali toa njia halali ambayo folda inapaswa kurejeshwa."; "getFolderIntent.error.noVaultSelected" = "Hakuna kuba iliyochaguliwa."; +"hubAuthentication.accessNotGranted" = "Kifaa chako bado hakijaidhinishwa kufikia kuba hii. Uliza mwenye kuba aidhinishe."; +"hubAuthentication.licenseExceeded" = "Mfano wako wa Cryptomator Hub una leseni batili. Tafadhali mjulishe msimamizi wa Hub ili kuboresha au kusasisha leseni."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Jina la Kifaa"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Ili kufikia kuba, kifaa chako kinahitaji kuidhinishwa na mmiliki wa kuba."; + "intents.saveFile.missingFile" = "Faili iliyotolewa si sahihi."; "intents.saveFile.invalidFolder" = "Faili iliyotolewa si sahihi."; "intents.saveFile.missingTemporaryFolder" = "Imeshindwa kuunda folda ya muda."; diff --git a/SharedResources/ta.lproj/Localizable.strings b/SharedResources/ta.lproj/Localizable.strings index e721d2546..a6cb77c22 100644 --- a/SharedResources/ta.lproj/Localizable.strings +++ b/SharedResources/ta.lproj/Localizable.strings @@ -8,6 +8,7 @@ "common.button.enable" = "இயக்கு"; "common.button.next" = "அடுத்து"; "common.button.ok" = "சரி"; +"common.button.refresh" = "புதுப்பி"; "common.button.remove" = "நீக்கு"; "common.button.retry" = "மீண்டும் முயற்சிக்கவும்"; "common.cells.url" = "URL"; diff --git a/SharedResources/te.lproj/Localizable.strings b/SharedResources/te.lproj/Localizable.strings index e0554efb6..95a4286ac 100644 --- a/SharedResources/te.lproj/Localizable.strings +++ b/SharedResources/te.lproj/Localizable.strings @@ -4,6 +4,7 @@ "common.button.edit" = "మార్పు"; "common.button.enable" = "ప్రారంభించు"; "common.button.ok" = "సరే"; +"common.button.refresh" = "రిఫ్రెష్ చేయండి"; "common.button.remove" = "తొలగించు"; "common.button.retry" = "మళ్ళీ చేయండి"; "common.cells.url" = "URL"; diff --git a/SharedResources/tr.lproj/Localizable.strings b/SharedResources/tr.lproj/Localizable.strings index dd9f6357d..59f13b30e 100644 --- a/SharedResources/tr.lproj/Localizable.strings +++ b/SharedResources/tr.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Etkinleştir"; "common.button.next" = "İleri"; "common.button.ok" = "Tamam"; +"common.button.refresh" = "Yenile"; +"common.button.register" = "Kaydol"; "common.button.remove" = "Kaldır"; "common.button.retry" = "Yeniden dene"; "common.button.signOut" = "Çıkış Yap"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Kasa nerede bulunuyor?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator \"%@\" kasasını algıladı.\nBu kasayı eklemek ister misiniz?"; "addVault.openExistingVault.detectedMasterkey.add" = "Bu Kasayı Ekle"; +"addVault.openExistingVault.downloadVault.progress" = "Kasa indiriliyor…"; "addVault.openExistingVault.password.footer" = "\"%@\" için şifre girin."; "addVault.openExistingVault.progress" = "Kasa ekleniyor…"; "addVault.success.info" = "\"%@\" kasası başarıyla eklendi.\nDosyalar uygulamasından bu kasaya erişebilirsiniz."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Dizin sağlanmadı. Lütfen bir klasörün döndürüleceği geçerli bir dizin sağlayın."; "getFolderIntent.error.noVaultSelected" = "Herhangi bir kasa seçili değil."; + +"hubAuthentication.title" = "Hub Kasası"; +"hubAuthentication.accessNotGranted" = "Cihazınıza henüz bu kasaya erişim yetkisi verilmedi. Kasa sahibinden yetkilendirmesini isteyin."; +"hubAuthentication.licenseExceeded" = "Cryptomator Hub örneğinizde geçersiz bir lisans var. Lisansı yükseltmesi veya yenilemesi için lütfen bir Hub yöneticisini bilgilendirin."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Cihaz adı"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Bu cihazdan ilk Hub erişimi gibi görünüyor. Erişim yetkilendirmesini tanımlamak için bu cihazı isimlendirmeniz gerekir."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Yeni uygulamalardan veya tarayıcılardan giriş yapmak için Hesap Anahtarınız gereklidir. Profilinizde bulunabilir."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Cihaz Kaydı Başarılı"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Kasaya erişmek için cihazınızın kasa sahibi tarafından yetkilendirilmesi gerekir."; +"hubAuthentication.requireAccountInit.alert.title" = "Eylem Gerekiyor"; +"hubAuthentication.requireAccountInit.alert.message" = "Devam etmek için lütfen Hub kullanıcı profilinizde gerekli adımları tamamlayın."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Profile Git"; + "intents.saveFile.missingFile" = "Sağlanan dosya geçersiz."; "intents.saveFile.invalidFolder" = "Sağlanan klasör geçersiz."; "intents.saveFile.missingTemporaryFolder" = "Geçici klasör oluşturulamadı."; diff --git a/SharedResources/uk.lproj/Localizable.strings b/SharedResources/uk.lproj/Localizable.strings index 32912f94b..dad5c5a84 100644 --- a/SharedResources/uk.lproj/Localizable.strings +++ b/SharedResources/uk.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "Увімкнути"; "common.button.next" = "Далі"; "common.button.ok" = "Гаразд"; +"common.button.refresh" = "Оновити"; "common.button.remove" = "Прибрати"; "common.button.retry" = "Повторити"; "common.button.signOut" = "Вийти"; @@ -58,6 +59,14 @@ "fileProvider.onboarding.button.openCryptomator" = "Відкрити Cryptomator"; "fileProvider.error.biometricalAuthWrongPassword.title" = "Невірний пароль"; "fileProvider.error.unlockButton" = "Розблокувати"; +"hubAuthentication.accessNotGranted" = "Ваш пристрій ще не має прав доступу до цього vault. Попросіть власника vault надати їх."; +"hubAuthentication.licenseExceeded" = "У вашого Cryptomator Hub недійсна ліцензія. Будь ласка, повідомте адміністратору Hub, що потрібно оновити або продовжити ліцензію."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Назва пристрою"; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Ваш Account Key необхідний для входу в систему з нових програм або браузерів. Його можна знайти в профілі."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Щоб отримати доступ до vault, ваш пристрій повинен бути авторизований власником vault."; +"hubAuthentication.requireAccountInit.alert.title" = "Необхідна дія"; +"hubAuthentication.requireAccountInit.alert.message" = "Щоб продовжити, будь ласка, виконайте необхідні кроки у вашому профілі користувача Hub."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Перейти до профілю"; "keepUnlockedDuration.auto.shortDisplayName" = "Автоматично"; "localFileSystemAuthentication.openExistingVault.button" = "Виберіть папку сховища"; @@ -91,6 +100,7 @@ "settings.clearCache" = "Очистити кеш"; "settings.cloudServices" = "Хмарні сервіси"; "settings.debugMode" = "Режим вiдладки"; +"settings.debugMode.alert.message" = "У цьому режимі чутливі дані можуть бути записані в файл журналу на вашому пристрої (наприклад, імена файлів і шляхи). Паролі, файли cookie тощо не включаються.\n\nНе забудьте вимкнути режим налагодження за першої нагоди."; "settings.rateApp" = "Оцінити додаток"; "settings.sendLogFile" = "Надіслати лог-файл"; "settings.unlockFullVersion" = "Розблокувати повну версію"; diff --git a/SharedResources/vi.lproj/Localizable.strings b/SharedResources/vi.lproj/Localizable.strings index 4f8700fa5..b3a4d9e41 100644 --- a/SharedResources/vi.lproj/Localizable.strings +++ b/SharedResources/vi.lproj/Localizable.strings @@ -9,6 +9,7 @@ "common.button.enable" = "Bật"; "common.button.next" = "Tiếp"; "common.button.ok" = "OK"; +"common.button.refresh" = "Làm mới"; "common.button.remove" = "Xóa"; "common.button.retry" = "Thử lại"; "common.cells.password" = "Mật khẩu"; @@ -21,13 +22,25 @@ "addVault.createNewVault.chooseCloud.header" = "Cryptomator nên lưu trữ các tệp được mã hóa trong vault của bạn ở đâu?"; "addVault.createNewVault.password.confirmPassword.alert.message" = "QUAN TRỌNG: Nếu bạn quên mật khẩu, không có cách nào để khôi phục dữ liệu của bạn."; "addVault.openExistingVault.title" = "Mở Vault Hiện Có"; + +"biometryType.faceID" = "Face ID"; +"biometryType.touchID" = "Touch ID"; "fileProvider.error.unlockButton" = "Mở khoá"; +"hubAuthentication.accessNotGranted" = "Thiết bị của bạn chưa được phép truy cập vault này. Yêu cầu chủ sở hữu cấp phép."; +"hubAuthentication.licenseExceeded" = "Phiên bản Cryptomator Hub của bạn có giấy phép không hợp lệ. Vui lòng thông báo cho quản trị viên Hub để nâng cấp hoặc gia hạn giấy phép."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Tên thiết bị"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Để truy cập vault, thiết bị của bạn cần được chủ sở hữu cho phép."; "purchase.retry.button" = "Thử lại"; "settings.title" = "Cài đặt"; "settings.clearCache" = "Xóa bộ nhớ Cache"; +"settings.debugMode.alert.message" = "Ở chế độ này, dữ liệu nhạy cảm có thể được ghi vào tệp nhật ký trên thiết bị của bạn (ví dụ: tên tệp và đường dẫn). Mật khẩu, cookie, v.v. được loại trừ rõ ràng.\n\nHãy nhớ tắt chế độ gỡ lỗi càng sớm càng tốt."; "s3Authentication.displayName" = "Tên hiển thị"; +"s3Authentication.accessKey" = "Khoá Truy cập"; +"s3Authentication.secretKey" = "Khoá Bí mật"; +"s3Authentication.existingBucket" = "Bucket Hiện có"; +"s3Authentication.endpoint" = "Điểm cuối"; "s3Authentication.region" = "Khu vực"; "unlockVault.button.unlock" = "Mở khoá"; diff --git a/SharedResources/zh-HK.lproj/Localizable.strings b/SharedResources/zh-HK.lproj/Localizable.strings index 97a46b73b..ee2ee3ac8 100644 --- a/SharedResources/zh-HK.lproj/Localizable.strings +++ b/SharedResources/zh-HK.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "啟用"; "common.button.next" = "繼續"; "common.button.ok" = "確認"; +"common.button.refresh" = "重新整理"; "common.button.remove" = "移除"; "common.button.retry" = "重試"; "common.button.signOut" = "登出"; @@ -110,6 +111,11 @@ "getFolderIntent.error.missingPath" = "沒有提供路徑。請提供應返回資料夾的有效路徑。"; "getFolderIntent.error.noVaultSelected" = "未選擇任何加密庫。"; +"hubAuthentication.accessNotGranted" = "您的設備權限尚未允許存取加密庫,請聯絡加密庫擁有者"; +"hubAuthentication.licenseExceeded" = "此 Cryptomator Hub 實例授權無效,請聯繫管理員升級或續訂授權。"; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "設備名稱"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "想讀取檔加密庫,你的設備需得到檔案庫擁有者的授權。"; + "intents.saveFile.missingFile" = "提供的檔案無效。"; "intents.saveFile.invalidFolder" = "提供的資料夾無效。"; "intents.saveFile.missingTemporaryFolder" = "無法建立臨時資料夾。"; diff --git a/SharedResources/zh-Hans.lproj/Localizable.strings b/SharedResources/zh-Hans.lproj/Localizable.strings index 695e47f50..3b016e7fe 100644 --- a/SharedResources/zh-Hans.lproj/Localizable.strings +++ b/SharedResources/zh-Hans.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "启用"; "common.button.next" = "下一步"; "common.button.ok" = "确定"; +"common.button.refresh" = "刷新"; +"common.button.register" = "注册"; "common.button.remove" = "删除"; "common.button.retry" = "重试"; "common.button.signOut" = "退出登录"; @@ -46,7 +48,7 @@ "addVault.createNewVault.chooseCloud.header" = "Cryptomator 应该在哪里存储您保险库的加密文件?"; "addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" 已存在于此位置,请更换保险库名称或位置。"; "addVault.createNewVault.detectedMasterkey.text" = "Cryptomator 已在此路径检测到保险库。\n若想创建一个新的保险库,请返回并选择其他路径。"; -"addVault.createNewVault.password.enterPassword.header" = "输入新密码。"; +"addVault.createNewVault.password.enterPassword.header" = "输入新密码"; "addVault.createNewVault.password.confirmPassword.header" = "确认新密码。"; "addVault.createNewVault.password.confirmPassword.alert.title" = "确认密码?"; "addVault.createNewVault.password.confirmPassword.alert.message" = "重要提示:一旦您忘记密码,将永远无法恢复您的数据。"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "该保险库的路径在哪?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator 检测到保险库 \"%@\"。\n您想要添加它吗?"; "addVault.openExistingVault.detectedMasterkey.add" = "添加此保险库"; +"addVault.openExistingVault.downloadVault.progress" = "正在下载保险库…"; "addVault.openExistingVault.password.footer" = "输入 \"%@\" 的密码。"; "addVault.openExistingVault.progress" = "正在添加保险库……"; "addVault.success.info" = "成功添加保险库 \"%@\"。\n现在可通过文管应用访问它。"; @@ -68,7 +71,7 @@ "changePassword.error.invalidOldPassword" = "当前密码错误,请重试。"; "changePassword.header.currentPassword.title" = "请输入当前密码。"; -"changePassword.header.newPassword.title" = "输入新密码。"; +"changePassword.header.newPassword.title" = "输入新密码"; "changePassword.header.newPasswordConfirmation.title" = "确认新密码。"; "changePassword.progress" = "正在修改密码……"; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "未提供路径,请提供文件夹应转到的有效路径。"; "getFolderIntent.error.noVaultSelected" = "未选择任何保险库。"; + +"hubAuthentication.title" = "Hub 保险库"; +"hubAuthentication.accessNotGranted" = "您的设备尚未授权访问此保险库,请联系保险库所有者,"; +"hubAuthentication.licenseExceeded" = "此 Cryptomator Hub 实例许可证无效,请联系Hub管理员升级或者续订许可证。"; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "设备名称"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "这似乎是设备的首次 Hub 访问。为了识别它以进行访问授权,您需要命名此设备"; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "从新应用或浏览器登录需要您的账户密钥,您可以在个人中心找到它"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "注册设备成功"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "要访问保险库,设备需得到保险库所有者的授权。"; +"hubAuthentication.requireAccountInit.alert.title" = "需要操作"; +"hubAuthentication.requireAccountInit.alert.message" = "要继续,请完成 Hub 用户中心所需的步骤"; +"hubAuthentication.requireAccountInit.alert.actionButton" = "前往个人中心"; + "intents.saveFile.missingFile" = "提供的文件无效。"; "intents.saveFile.invalidFolder" = "提供的文件夹无效。"; "intents.saveFile.missingTemporaryFolder" = "无法创建临时文件夹。"; diff --git a/SharedResources/zh-Hant.lproj/Localizable.strings b/SharedResources/zh-Hant.lproj/Localizable.strings index 194b20c31..a396ab7fc 100644 --- a/SharedResources/zh-Hant.lproj/Localizable.strings +++ b/SharedResources/zh-Hant.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "啟用"; "common.button.next" = "繼續"; "common.button.ok" = "確認"; +"common.button.refresh" = "重新整理"; "common.button.remove" = "移除"; "common.button.retry" = "重試"; "common.button.signOut" = "登出"; @@ -110,6 +111,11 @@ "getFolderIntent.error.missingPath" = "沒有提供路徑。請提供應返回資料夾的有效路徑。"; "getFolderIntent.error.noVaultSelected" = "未選擇任何加密檔案庫。"; +"hubAuthentication.accessNotGranted" = "您的設備權限尚未允許存取檔案庫,請聯絡檔案庫擁有者"; +"hubAuthentication.licenseExceeded" = "此 Cryptomator Hub 實例授權無效,請聯繫管理員升級或續訂授權。"; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "設備名稱"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "想讀取檔案庫,你的設備需得到檔案庫擁有者的授權。"; + "intents.saveFile.missingFile" = "提供的檔案無效。"; "intents.saveFile.invalidFolder" = "提供的資料夾無效。"; "intents.saveFile.missingTemporaryFolder" = "無法建立臨時資料夾。"; diff --git a/fastlane/changelog.txt b/fastlane/changelog.txt index 995f6a12f..f38d2e1d6 100644 --- a/fastlane/changelog.txt +++ b/fastlane/changelog.txt @@ -1,2 +1,3 @@ -- Added Hebrew translation -- Fixed "not found" error when listing a directory with unreachable files in iCloud Drive (#313) \ No newline at end of file +- Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332, #333) +- New vaults are now created with GCM encryption by default (#288) +- Added Bangla, Filipino, and Hungarian translations \ No newline at end of file diff --git a/fastlane/config/freemium/metadata/copyright.txt b/fastlane/config/freemium/metadata/copyright.txt index 0364a6b33..65cbf78b5 100644 --- a/fastlane/config/freemium/metadata/copyright.txt +++ b/fastlane/config/freemium/metadata/copyright.txt @@ -1 +1 @@ -2023 cryptomator.org \ No newline at end of file +2024 cryptomator.org \ No newline at end of file diff --git a/fastlane/config/freemium/metadata/de-DE/release_notes.txt b/fastlane/config/freemium/metadata/de-DE/release_notes.txt index 3e52e6c8b..c020ba9c2 100644 --- a/fastlane/config/freemium/metadata/de-DE/release_notes.txt +++ b/fastlane/config/freemium/metadata/de-DE/release_notes.txt @@ -1,2 +1,3 @@ -- Hebräische Übersetzung hinzugefügt -- Fehler "nicht gefunden" beim Auflisten eines Verzeichnisses mit nicht erreichbaren Dateien in iCloud Drive behoben (#313) \ No newline at end of file +- Unterstützung für Cryptomator Hub hinzugefügt (#315, #322, #323, #326, #329, #332, #333) +- Neue Tresore werden nun standardmäßig mit GCM-Verschlüsselung erstellt (#288) +- Übersetzungen für Bangla, Filipino und Ungarisch hinzugefügt \ No newline at end of file diff --git a/fastlane/config/freemium/metadata/en-US/release_notes.txt b/fastlane/config/freemium/metadata/en-US/release_notes.txt index 995f6a12f..f38d2e1d6 100644 --- a/fastlane/config/freemium/metadata/en-US/release_notes.txt +++ b/fastlane/config/freemium/metadata/en-US/release_notes.txt @@ -1,2 +1,3 @@ -- Added Hebrew translation -- Fixed "not found" error when listing a directory with unreachable files in iCloud Drive (#313) \ No newline at end of file +- Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332, #333) +- New vaults are now created with GCM encryption by default (#288) +- Added Bangla, Filipino, and Hungarian translations \ No newline at end of file diff --git a/fastlane/config/premium/metadata/copyright.txt b/fastlane/config/premium/metadata/copyright.txt index 0364a6b33..65cbf78b5 100644 --- a/fastlane/config/premium/metadata/copyright.txt +++ b/fastlane/config/premium/metadata/copyright.txt @@ -1 +1 @@ -2023 cryptomator.org \ No newline at end of file +2024 cryptomator.org \ No newline at end of file diff --git a/fastlane/config/premium/metadata/de-DE/release_notes.txt b/fastlane/config/premium/metadata/de-DE/release_notes.txt index 3e52e6c8b..c020ba9c2 100644 --- a/fastlane/config/premium/metadata/de-DE/release_notes.txt +++ b/fastlane/config/premium/metadata/de-DE/release_notes.txt @@ -1,2 +1,3 @@ -- Hebräische Übersetzung hinzugefügt -- Fehler "nicht gefunden" beim Auflisten eines Verzeichnisses mit nicht erreichbaren Dateien in iCloud Drive behoben (#313) \ No newline at end of file +- Unterstützung für Cryptomator Hub hinzugefügt (#315, #322, #323, #326, #329, #332, #333) +- Neue Tresore werden nun standardmäßig mit GCM-Verschlüsselung erstellt (#288) +- Übersetzungen für Bangla, Filipino und Ungarisch hinzugefügt \ No newline at end of file diff --git a/fastlane/config/premium/metadata/en-US/release_notes.txt b/fastlane/config/premium/metadata/en-US/release_notes.txt index 995f6a12f..f38d2e1d6 100644 --- a/fastlane/config/premium/metadata/en-US/release_notes.txt +++ b/fastlane/config/premium/metadata/en-US/release_notes.txt @@ -1,2 +1,3 @@ -- Added Hebrew translation -- Fixed "not found" error when listing a directory with unreachable files in iCloud Drive (#313) \ No newline at end of file +- Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332, #333) +- New vaults are now created with GCM encryption by default (#288) +- Added Bangla, Filipino, and Hungarian translations \ No newline at end of file