├── .github
└── workflows
│ ├── README.md
│ ├── build.yml
│ └── publish-release.yml
├── .gitignore
├── .junie
└── guidelines.md
├── LICENSE
├── PRIVACY.md
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── kotlin
│ │ └── com
│ │ └── darkrockstudios
│ │ └── app
│ │ └── securecamera
│ │ └── SmokeTestUiTest.kt
│ ├── debug
│ └── res
│ │ └── values
│ │ ├── ic_launcher_background.xml
│ │ └── strings.xml
│ ├── full
│ ├── AndroidManifest.xml
│ └── kotlin
│ │ └── com
│ │ └── darkrockstudios
│ │ └── app
│ │ └── securecamera
│ │ ├── FullSnapSafeApplication.kt
│ │ └── obfuscation
│ │ └── MlFacialDetection.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── kotlin
│ │ └── com
│ │ │ └── darkrockstudios
│ │ │ └── app
│ │ │ └── securecamera
│ │ │ ├── App.kt
│ │ │ ├── AppModule.kt
│ │ │ ├── BaseViewModel.kt
│ │ │ ├── CameraVectorImages.kt
│ │ │ ├── ConfirmDeletePhotoDialog.kt
│ │ │ ├── ImageUtils.kt
│ │ │ ├── KeepScreenOnEffect.kt
│ │ │ ├── LocationRepository.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── ReentrantMutex.kt
│ │ │ ├── ReleaseLogTree.kt
│ │ │ ├── RequestLocationPermission.kt
│ │ │ ├── SnapSafeApplication.kt
│ │ │ ├── TimeUtils.kt
│ │ │ ├── Util.kt
│ │ │ ├── about
│ │ │ └── AboutContent.kt
│ │ │ ├── auth
│ │ │ ├── AuthorizationRepository.kt
│ │ │ ├── PinVerificationContent.kt
│ │ │ ├── PinVerificationViewModel.kt
│ │ │ └── SessionService.kt
│ │ │ ├── camera
│ │ │ ├── BottomCameraControls.kt
│ │ │ ├── CameraContent.kt
│ │ │ ├── CameraControls.kt
│ │ │ ├── CameraPreview.kt
│ │ │ ├── CameraState.kt
│ │ │ ├── CapturedImage.kt
│ │ │ ├── ImageUtils.kt
│ │ │ ├── LevelIndicator.kt
│ │ │ ├── NoCameraPermission.kt
│ │ │ ├── PermissionRationaleDialog.kt
│ │ │ ├── PhotoDef.kt
│ │ │ ├── PhotoMetaData.kt
│ │ │ ├── SecureImageRepository.kt
│ │ │ ├── ThumbnailCache.kt
│ │ │ ├── TopCameraControlsBar.kt
│ │ │ ├── ZoomMeter.kt
│ │ │ └── tiffOrientation.kt
│ │ │ ├── gallery
│ │ │ ├── GalleryContent.kt
│ │ │ ├── GalleryTopNav.kt
│ │ │ ├── GalleryViewModel.kt
│ │ │ ├── rememberPhotoPickerLauncher.kt
│ │ │ └── vibrateDevice.kt
│ │ │ ├── import
│ │ │ ├── ImportCancelReceiver.kt
│ │ │ ├── ImportPhotosContent.kt
│ │ │ ├── ImportPhotosTopBar.kt
│ │ │ ├── ImportPhotosViewModel.kt
│ │ │ └── ImportWorker.kt
│ │ │ ├── introduction
│ │ │ ├── IntroductionContent.kt
│ │ │ ├── IntroductionSlide.kt
│ │ │ ├── IntroductionViewModel.kt
│ │ │ ├── PinCreationContent.kt
│ │ │ └── SecurityContent.kt
│ │ │ ├── navigation
│ │ │ ├── AppDestinations.kt
│ │ │ ├── AppNavigation.kt
│ │ │ └── NavAnimations.kt
│ │ │ ├── obfuscation
│ │ │ ├── FacialDetection.kt
│ │ │ ├── ObfuscatePhotoContent.kt
│ │ │ ├── ObfuscatePhotoTopBar.kt
│ │ │ ├── ObfuscatePhotoViewModel.kt
│ │ │ ├── ObfuscationTools.kt
│ │ │ └── Region.kt
│ │ │ ├── preferences
│ │ │ ├── AppPreferencesDataSource.kt
│ │ │ └── XorCipher.kt
│ │ │ ├── security
│ │ │ ├── KeyParams.kt
│ │ │ ├── SchemeConfig.kt
│ │ │ ├── SecurityLevel.kt
│ │ │ ├── SharedKey.kt
│ │ │ ├── deviceInfo.kt
│ │ │ ├── pin
│ │ │ │ ├── PinRepository.kt
│ │ │ │ ├── PinRepositoryHardware.kt
│ │ │ │ └── PinRepositorySoftware.kt
│ │ │ └── schemes
│ │ │ │ ├── EncryptionScheme.kt
│ │ │ │ ├── HardwareBackedEncryptionScheme.kt
│ │ │ │ └── SoftwareEncryptionScheme.kt
│ │ │ ├── settings
│ │ │ ├── DecoyPhotoExplanationDialog.kt
│ │ │ ├── LocationDialog.kt
│ │ │ ├── PoisonPillDialog.kt
│ │ │ ├── PoisonPillPinCreationDialog.kt
│ │ │ ├── RemovePoisonPillDialog.kt
│ │ │ ├── SecurityResetDialog.kt
│ │ │ ├── SettingsContent.kt
│ │ │ └── SettingsViewModel.kt
│ │ │ ├── share
│ │ │ ├── DecryptingImageProvider.kt
│ │ │ └── ShareUtils.kt
│ │ │ ├── ui
│ │ │ ├── NotificationPermissionRationale.kt
│ │ │ ├── PlayfulScaleVisibility.kt
│ │ │ ├── UiEvent.kt
│ │ │ ├── UiEventHandler.kt
│ │ │ └── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ ├── usecases
│ │ │ ├── CreatePinUseCase.kt
│ │ │ ├── InvalidateSessionUseCase.kt
│ │ │ ├── MigratePinHash.kt
│ │ │ ├── PinSizeUseCase.kt
│ │ │ ├── PinStrengthCheckUseCase.kt
│ │ │ ├── RemovePoisonPillIUseCase.kt
│ │ │ ├── SecurityResetUseCase.kt
│ │ │ └── VerifyPinUseCase.kt
│ │ │ └── viewphoto
│ │ │ ├── PhotoInfoDialog.kt
│ │ │ ├── ViewPhotoContent.kt
│ │ │ ├── ViewPhotoTopBar.kt
│ │ │ └── ViewPhotoViewModel.kt
│ └── res
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_launcher_foreground.xml
│ │ └── ic_launcher_monochrome.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values-de
│ │ └── strings.xml
│ │ ├── values-es
│ │ └── strings.xml
│ │ ├── values-fr
│ │ └── strings.xml
│ │ ├── values-it
│ │ └── strings.xml
│ │ ├── values-pt-rBR
│ │ └── strings.xml
│ │ ├── values-uk
│ │ └── strings.xml
│ │ ├── values-zh-rCN
│ │ └── strings.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ ├── data_extraction_rules.xml
│ │ └── file_paths.xml
│ ├── oss
│ ├── AndroidManifest.xml
│ └── kotlin
│ │ └── com
│ │ └── darkrockstudios
│ │ └── app
│ │ └── securecamera
│ │ ├── OssSnapSafeApplication.kt
│ │ └── obfuscation
│ │ └── AndroidFacialDetection.kt
│ └── test
│ ├── kotlin
│ └── com
│ │ └── darkrockstudios
│ │ └── app
│ │ └── securecamera
│ │ ├── TestClock.kt
│ │ ├── auth
│ │ └── AuthorizationManagerTest.kt
│ │ ├── camera
│ │ └── PhotoDefTest.kt
│ │ ├── imagemanager
│ │ └── SecureImageRepositoryTest.kt
│ │ ├── navigation
│ │ └── AppDestinationsTest.kt
│ │ ├── preferences
│ │ ├── AppPreferencesManagerTest.kt
│ │ └── XorCipherTest.kt
│ │ ├── security
│ │ └── ShardedKeyTest.kt
│ │ └── usecases
│ │ ├── PinSizeUseCaseTest.kt
│ │ ├── PinStrengthCheckUseCaseTest.kt
│ │ └── VerifyPinUseCaseTest.kt
│ └── resources
│ └── red.jpg
├── build.gradle.kts
├── docs
├── SnapSafe Attack Vectors.md
├── SnapSafe Related Incidents.md
└── SnapSafe Security on Android.md
├── fastlane
├── Appfile
├── Fastfile
├── README.md
└── metadata
│ └── android
│ ├── de-DE
│ ├── full_description.txt
│ ├── images
│ │ ├── featureGraphic.png
│ │ ├── icon.png
│ │ ├── phoneScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ ├── 2_en-US.png
│ │ │ ├── 3_en-US.png
│ │ │ ├── 4_en-US.png
│ │ │ └── 5_en-US.png
│ │ ├── sevenInchScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ ├── 2_en-US.png
│ │ │ └── 3_en-US.png
│ │ └── tenInchScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ └── 2_en-US.png
│ ├── short_description.txt
│ └── title.txt
│ ├── en-US
│ ├── changelogs
│ │ ├── 10.txt
│ │ ├── 12.txt
│ │ ├── 13.txt
│ │ ├── 14.txt
│ │ ├── 15.txt
│ │ ├── 16.txt
│ │ ├── 17.txt
│ │ ├── 3.txt
│ │ ├── 4.txt
│ │ └── 8.txt
│ ├── full_description.txt
│ ├── images
│ │ ├── featureGraphic.png
│ │ ├── icon.png
│ │ ├── phoneScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ ├── 2_en-US.png
│ │ │ ├── 3_en-US.png
│ │ │ ├── 4_en-US.png
│ │ │ └── 5_en-US.png
│ │ ├── sevenInchScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ ├── 2_en-US.png
│ │ │ └── 3_en-US.png
│ │ └── tenInchScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ └── 2_en-US.png
│ ├── short_description.txt
│ └── title.txt
│ ├── es-ES
│ ├── full_description.txt
│ ├── images
│ │ ├── featureGraphic.png
│ │ ├── icon.png
│ │ ├── phoneScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ ├── 2_en-US.png
│ │ │ ├── 3_en-US.png
│ │ │ ├── 4_en-US.png
│ │ │ └── 5_en-US.png
│ │ ├── sevenInchScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ ├── 2_en-US.png
│ │ │ └── 3_en-US.png
│ │ └── tenInchScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ └── 2_en-US.png
│ ├── short_description.txt
│ └── title.txt
│ ├── fr-FR
│ ├── full_description.txt
│ ├── images
│ │ ├── featureGraphic.png
│ │ ├── icon.png
│ │ ├── phoneScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ ├── 2_en-US.png
│ │ │ ├── 3_en-US.png
│ │ │ ├── 4_en-US.png
│ │ │ └── 5_en-US.png
│ │ ├── sevenInchScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ ├── 2_en-US.png
│ │ │ └── 3_en-US.png
│ │ └── tenInchScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ └── 2_en-US.png
│ ├── short_description.txt
│ └── title.txt
│ ├── it-IT
│ ├── full_description.txt
│ ├── images
│ │ ├── featureGraphic.png
│ │ ├── icon.png
│ │ ├── phoneScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ ├── 2_en-US.png
│ │ │ ├── 3_en-US.png
│ │ │ ├── 4_en-US.png
│ │ │ └── 5_en-US.png
│ │ ├── sevenInchScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ ├── 2_en-US.png
│ │ │ └── 3_en-US.png
│ │ └── tenInchScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ └── 2_en-US.png
│ ├── short_description.txt
│ └── title.txt
│ ├── pt-BR
│ ├── full_description.txt
│ ├── images
│ │ ├── featureGraphic.png
│ │ ├── icon.png
│ │ ├── phoneScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ ├── 2_en-US.png
│ │ │ ├── 3_en-US.png
│ │ │ ├── 4_en-US.png
│ │ │ └── 5_en-US.png
│ │ ├── sevenInchScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ ├── 2_en-US.png
│ │ │ └── 3_en-US.png
│ │ └── tenInchScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ └── 2_en-US.png
│ ├── short_description.txt
│ └── title.txt
│ ├── uk
│ ├── full_description.txt
│ ├── images
│ │ ├── featureGraphic.png
│ │ ├── icon.png
│ │ ├── phoneScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ ├── 2_en-US.png
│ │ │ ├── 3_en-US.png
│ │ │ ├── 4_en-US.png
│ │ │ └── 5_en-US.png
│ │ ├── sevenInchScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ ├── 2_en-US.png
│ │ │ └── 3_en-US.png
│ │ └── tenInchScreenshots
│ │ │ ├── 1_en-US.png
│ │ │ └── 2_en-US.png
│ ├── short_description.txt
│ └── title.txt
│ └── zh-CN
│ ├── full_description.txt
│ ├── images
│ ├── featureGraphic.png
│ ├── icon.png
│ ├── phoneScreenshots
│ │ ├── 1_en-US.png
│ │ ├── 2_en-US.png
│ │ ├── 3_en-US.png
│ │ ├── 4_en-US.png
│ │ └── 5_en-US.png
│ ├── sevenInchScreenshots
│ │ ├── 1_en-US.png
│ │ ├── 2_en-US.png
│ │ └── 3_en-US.png
│ └── tenInchScreenshots
│ │ ├── 1_en-US.png
│ │ └── 2_en-US.png
│ ├── short_description.txt
│ └── title.txt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── renovate.json
└── settings.gradle.kts
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | on:
4 | push:
5 | branches: [ "**" ]
6 | pull_request:
7 | branches: [ "**" ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | api-level: [ 34 ]
15 |
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | - name: Set up JDK
20 | uses: actions/setup-java@v4
21 | with:
22 | distribution: 'temurin'
23 | java-version: '17'
24 |
25 | - name: Setup Gradle
26 | uses: gradle/actions/setup-gradle@v4
27 |
28 | - name: Grant execute permission for gradlew
29 | run: chmod +x gradlew
30 |
31 | - name: Decode Keystore
32 | env:
33 | ENCODED_KEYSTORE: ${{ secrets.ENCODED_KEYSTORE }}
34 | run: |
35 | echo $ENCODED_KEYSTORE | base64 -d > app/keystore.jks
36 |
37 | - name: Run Unit Tests
38 | run: ./gradlew test
39 |
40 | # - name: Enable KVM
41 | # run: |
42 | # echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
43 | # sudo udevadm control --reload-rules
44 | # sudo udevadm trigger --name-match=kvm
45 | #
46 | # - name: AVD cache
47 | # uses: actions/cache@v4
48 | # id: avd-cache
49 | # with:
50 | # path: |
51 | # ~/.android/avd/*
52 | # ~/.android/adb*
53 | # key: avd-${{ matrix.api-level }}
54 | #
55 | # - name: create AVD and generate snapshot for caching
56 | # if: steps.avd-cache.outputs.cache-hit != 'true'
57 | # uses: reactivecircus/android-emulator-runner@v2
58 | # with:
59 | # api-level: ${{ matrix.api-level }}
60 | # arch: x86_64
61 | # target: default
62 | # force-avd-creation: false
63 | # disable-animations: false
64 | # script: echo "Generated AVD snapshot for caching."
65 | #
66 | # - name: Run Instrumentation Tests
67 | # uses: reactivecircus/android-emulator-runner@v2
68 | # with:
69 | # api-level: ${{ matrix.api-level }}
70 | # arch: x86_64
71 | # target: default
72 | # force-avd-creation: false
73 | # emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-front emulated -camera-back emulated
74 | # disable-animations: true
75 | # script: |
76 | # adb wait-for-device
77 | # adb shell input keyevent 82
78 | # ./gradlew connectedOssDebugAndroidTest --continue
79 |
80 | - name: Upload Test Reports
81 | if: failure()
82 | uses: actions/upload-artifact@v4
83 | with:
84 | name: android-test-report
85 | path: '**/build/reports/'
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | .idea
17 | app/release
18 |
--------------------------------------------------------------------------------
/.junie/guidelines.md:
--------------------------------------------------------------------------------
1 | # Project Guidelines
2 |
3 | We're creating an Android app that is a privacy focused camera.
4 |
5 | We want to take pictures, strip all metadata, store them in app-private encrypted storage
6 | and require a PIN separate from the device's PIN to view them.
7 |
8 | With all of that privacy in mind, we do want to be able to use the normal Android "share"
9 | system for getting the images out of our secure storage and into other apps.
10 |
11 | As a stretch goal, I want to provide further security features such as automatic face
12 | blurring.
13 |
14 | * The project consists of a single `app` module which is the whole application
15 | * We're using Kotlin
16 | * Prefer KMP libraries over Kotlin/JVM when possible
17 | * We're using Compose for the UI
18 | * We use tabs, no spaces
19 | * We should write unit tests when possible for new functions or features
20 | * We use Mockk for our test mocking
21 | * Comment code only sparingly. Obvious comments should be left out, only comment on code that is slight weird or complex
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025-2026 Adam Brown
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
1 | # Secure Camera – Privacy Policy
2 | *Effective April 20 2025*
3 |
4 | Secure Camera was built with privacy as its first‑class feature.
5 | Below is the plain‑language summary of what happens to your data when you use the app:
6 |
7 | | What we collect | Where it goes | Why we need it |
8 | |-----------------|--------------|----------------|
9 | | **Nothing**—no personal data, usage analytics, or diagnostics | Nowhere (the app has *no* Internet permission) | We don't |
10 |
11 | ---
12 |
13 | ## 1. Data Collection
14 | Secure Camera does **not** collect, store, or transmit any personally identifiable information (PII), usage data, or any other kind of data what so ever. All photos and videos remain solely on your device, unless you explicitly share them.
15 |
16 | ## 2. Network Access
17 | The app’s manifest deliberately omits `android.permission.INTERNET`. Consequently, Secure Camera is technically incapable of connecting to the Internet, cloud services, or third‑party APIs.
18 |
19 | ## 3. File Access
20 | Secure Camera operates entirely within its private app storage for saving images you capture. It does **not** request broad file‑system access (e.g., `MANAGE_EXTERNAL_STORAGE`) and never reads files outside its own sandbox.
21 |
22 | ## 4. Permissions Used
23 | * `android.permission.CAMERA` – required to capture photos and video.
24 | * `android.permission.ACCESS_COARSE_LOCATION` User's can optionally grant this permission if they want obfuscated location data saved with their photos.
25 | * `android.permission.ACCESS_FINE_LOCATION` User's can optionally grant this permission if they want precise location data saved with their photos.
26 |
27 | No other runtime permissions are requested.
28 |
29 | ## 5. Third‑Party Services
30 | None. Secure Camera is 100 % open source and contains no advertising or analytics libraries.
31 |
32 | ## 6. Changes to This Policy
33 | If Secure Camera’s behavior ever changes in a way that affects privacy, this document will be updated in the source repository and a new release will note those changes.
34 |
35 | ## 7. Contact
36 | Questions or concerns? Open an issue in the [GitHub repository](https://github.com/Wavesonics/SecureCamera).
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Snap Safe
2 | *The camera that minds its own business.*
3 |
4 | _Available on:_
5 |
6 | [](https://play.google.com/store/apps/details?id=com.darkrockstudios.app.securecamera)
7 | [](https://f-droid.org/en/packages/com.darkrockstudios.app.securecamera/)
8 | [](https://github.com/SecureCamera/SecureCameraAndroid/releases/latest)
9 |
10 | [snapsafe.org](https://snapsafe.org/)
11 |
12 | [](http://www.snapsafe.org)
13 |
14 | [](https://codebeat.co/projects/github-com-securecamera-securecameraandroid-main)
15 |
16 | ----
17 |
18 | ## Why Snap Safe?
19 |
20 | **SnapSafe** is a camera app that has been engineered from the ground up to protect your photos.
21 |
22 | Attacks come in many forms, from accidental swipes, to intrusive surveillance, and even malicious code.
23 | **SnapSafe** can protect your photos from all angles.
24 |
25 | ### Key Features
26 |
27 | * 🔒 **Zero‑Leak Design** – The app has no internet access; android backups are prevented..
28 | * 🛡️ **Fully Encrypted** – Shots are written to encrypted, app‑private storage.
29 | * 🔢 **PIN‑Locked Gallery** – A separate PIN stands between curious thumbs and your photos.
30 | * 📤 **Secure Sharing** – Metadata is scrubbed and filenames are randomized when you share.
31 | * 😶🌫️ **Auto-Face Blur** – Obfuscate faces automatically with our secure blur algorithm.
32 | * 🗺️ **Granular Location** – Add coarse, fine, or zero location data—your call.
33 | * ☠️ **Poison Pill** – Set a special PIN, that when entered, appears to work normally but actually deletes your existing
34 | photos.
35 | * 🎭 **Decoy Photos** – Select innocuous decoy photos, these will be preserved when your Poison Pill is activated.
36 | * 👀 **100 % Open Source** – Auditable code in plain sight.
37 |
38 | ### On the Roadmap
39 |
40 | * Encrypted video recording. _Maybe._
41 | * Improved photo-taking experience
42 |
43 | ## Read our papers on SnapSafe
44 |
45 | - [Security Design](docs/SnapSafe%20Security%20on%20Android.md)
46 | - [Attack Vectors](docs/SnapSafe%20Attack%20Vectors.md)
47 | - [Related Incidents](docs/SnapSafe%20Related%20Incidents.md)
48 |
49 | ---
50 |
51 | ## Contributing
52 |
53 | Pull requests are happily accepted.
54 |
55 | Start with an issue or draft PR and we can talk it through.
56 |
57 | ### Automated Publishing
58 |
59 | The project uses GitHub Actions to automatically build and publish new releases to Google Play when a tag with the
60 | format `v*` (e.g., `v1.0.0`) is pushed. See the [GitHub Actions workflow documentation](.github/workflows/README.md) for
61 | details on how this works and the required setup.
62 |
63 | The project includes a pre-configured [FastLane](https://fastlane.tools/) setup for automating the deployment process.
64 | See the [FastLane documentation](fastlane/README.md) for details on how to use it for manual deployments or to customize
65 | the metadata.
66 |
67 | ---
68 |
69 | ## License
70 |
71 | SnapSafe is released under the [MIT License](LICENSE). Use it, fork it, improve it—just keep it open.
72 |
73 | ---
74 |
75 | ## Privacy
76 |
77 | Our full, ultra‑brief Privacy Policy lives in [PRIVACY.md](PRIVACY.md). Spoiler: we collect nothing.
78 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/debug/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #9C27B0
4 |
--------------------------------------------------------------------------------
/app/src/debug/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SnapSafeDev
3 |
--------------------------------------------------------------------------------
/app/src/full/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/full/kotlin/com/darkrockstudios/app/securecamera/FullSnapSafeApplication.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import com.darkrockstudios.app.securecamera.obfuscation.FacialDetection
4 | import com.darkrockstudios.app.securecamera.obfuscation.MlFacialDetection
5 | import org.koin.core.module.Module
6 | import org.koin.core.module.dsl.factoryOf
7 | import org.koin.dsl.bind
8 | import org.koin.dsl.module
9 |
10 | class FullSnapSafeApplication : SnapSafeApplication() {
11 | override fun flavorModule(): Module = module {
12 | factoryOf(::MlFacialDetection) bind FacialDetection::class
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/full/kotlin/com/darkrockstudios/app/securecamera/obfuscation/MlFacialDetection.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.obfuscation
2 |
3 | import android.graphics.Bitmap
4 | import com.google.mlkit.vision.common.InputImage
5 | import com.google.mlkit.vision.face.FaceDetection
6 | import com.google.mlkit.vision.face.FaceDetectorOptions
7 | import com.google.mlkit.vision.face.FaceLandmark
8 | import kotlinx.coroutines.suspendCancellableCoroutine
9 | import timber.log.Timber
10 | import kotlin.coroutines.resume
11 |
12 | class MlFacialDetection : FacialDetection {
13 | private val detector = FaceDetection.getClient(
14 | FaceDetectorOptions.Builder()
15 | .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
16 | .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
17 | .setMinFaceSize(0.02f)
18 | .build()
19 | )
20 |
21 | override suspend fun processForFaces(bitmap: Bitmap): List {
22 | val inputImage = InputImage.fromBitmap(bitmap, 0)
23 |
24 | return suspendCancellableCoroutine { continuation ->
25 | detector.process(inputImage)
26 | .addOnSuccessListener { foundFaces ->
27 | val newRegions = foundFaces.map { face ->
28 | val leftEye =
29 | face.allLandmarks.find { it.landmarkType == FaceLandmark.LEFT_EYE }
30 | val rightEye =
31 | face.allLandmarks.find { it.landmarkType == FaceLandmark.RIGHT_EYE }
32 | val eyes = if (leftEye != null && rightEye != null) {
33 | FacialDetection.FoundFace.Eyes(
34 | left = leftEye.position,
35 | right = rightEye.position,
36 | )
37 | } else {
38 | null
39 | }
40 | FacialDetection.FoundFace(
41 | boundingBox = face.boundingBox,
42 | eyes = eyes
43 | )
44 | }
45 | continuation.resume(newRegions)
46 | }.addOnFailureListener { e ->
47 | Timber.Forest.e(e, "Failed face detection in Image")
48 | continuation.resume(emptyList())
49 | }
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
28 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
58 |
61 |
62 |
67 |
68 |
71 |
72 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/App.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import androidx.compose.foundation.layout.imePadding
4 | import androidx.compose.material3.Scaffold
5 | import androidx.compose.material3.SnackbarHost
6 | import androidx.compose.material3.SnackbarHostState
7 | import androidx.compose.runtime.*
8 | import androidx.compose.ui.Modifier
9 | import androidx.lifecycle.compose.LifecycleResumeEffect
10 | import androidx.navigation.NavHostController
11 | import com.darkrockstudios.app.securecamera.auth.AuthorizationRepository
12 | import com.darkrockstudios.app.securecamera.navigation.AppNavHost
13 | import com.darkrockstudios.app.securecamera.navigation.enforceAuth
14 | import com.darkrockstudios.app.securecamera.preferences.AppPreferencesDataSource
15 | import com.darkrockstudios.app.securecamera.ui.theme.SecureCameraTheme
16 | import org.koin.compose.KoinContext
17 | import org.koin.compose.koinInject
18 |
19 | @Composable
20 | fun App(
21 | capturePhoto: MutableState,
22 | startDestination: String,
23 | navController: NavHostController
24 | ) {
25 | KoinContext {
26 | SecureCameraTheme {
27 | val snackbarHostState = remember { SnackbarHostState() }
28 | val preferencesManager = koinInject()
29 | val authorizationRepository = koinInject()
30 |
31 | val hasCompletedIntro by preferencesManager.hasCompletedIntro.collectAsState(initial = null)
32 |
33 | VerifySessionOnResume(navController, hasCompletedIntro, authorizationRepository)
34 |
35 | if (hasCompletedIntro != null) {
36 | Scaffold(
37 | snackbarHost = { SnackbarHost(snackbarHostState) },
38 | modifier = Modifier.imePadding()
39 | ) { paddingValues ->
40 | AppNavHost(
41 | navController = navController,
42 | capturePhoto = capturePhoto,
43 | modifier = Modifier,
44 | snackbarHostState = snackbarHostState,
45 | startDestination = startDestination,
46 | paddingValues = paddingValues,
47 | )
48 | }
49 | }
50 | }
51 | }
52 | }
53 |
54 | @Composable
55 | private fun VerifySessionOnResume(
56 | navController: NavHostController,
57 | hasCompletedIntro: Boolean?,
58 | authorizationRepository: AuthorizationRepository
59 | ) {
60 | var requireAuthCheck = remember { false }
61 | LifecycleResumeEffect(hasCompletedIntro) {
62 | if (hasCompletedIntro == true && requireAuthCheck) {
63 | enforceAuth(authorizationRepository, navController.currentDestination, navController)
64 | }
65 | onPauseOrDispose {
66 | requireAuthCheck = true
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import androidx.work.WorkManager
4 | import com.darkrockstudios.app.securecamera.auth.AuthorizationRepository
5 | import com.darkrockstudios.app.securecamera.auth.PinVerificationViewModel
6 | import com.darkrockstudios.app.securecamera.camera.SecureImageRepository
7 | import com.darkrockstudios.app.securecamera.camera.ThumbnailCache
8 | import com.darkrockstudios.app.securecamera.gallery.GalleryViewModel
9 | import com.darkrockstudios.app.securecamera.import.ImportPhotosViewModel
10 | import com.darkrockstudios.app.securecamera.introduction.IntroductionViewModel
11 | import com.darkrockstudios.app.securecamera.obfuscation.ObfuscatePhotoViewModel
12 | import com.darkrockstudios.app.securecamera.preferences.AppPreferencesDataSource
13 | import com.darkrockstudios.app.securecamera.security.DeviceInfo
14 | import com.darkrockstudios.app.securecamera.security.SecurityLevel
15 | import com.darkrockstudios.app.securecamera.security.SecurityLevelDetector
16 | import com.darkrockstudios.app.securecamera.security.pin.PinRepository
17 | import com.darkrockstudios.app.securecamera.security.pin.PinRepositoryHardware
18 | import com.darkrockstudios.app.securecamera.security.pin.PinRepositorySoftware
19 | import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
20 | import com.darkrockstudios.app.securecamera.security.schemes.HardwareBackedEncryptionScheme
21 | import com.darkrockstudios.app.securecamera.security.schemes.SoftwareEncryptionScheme
22 | import com.darkrockstudios.app.securecamera.settings.SettingsViewModel
23 | import com.darkrockstudios.app.securecamera.usecases.*
24 | import com.darkrockstudios.app.securecamera.viewphoto.ViewPhotoViewModel
25 | import org.koin.core.module.dsl.factoryOf
26 | import org.koin.core.module.dsl.singleOf
27 | import org.koin.core.module.dsl.viewModelOf
28 | import org.koin.dsl.bind
29 | import org.koin.dsl.module
30 | import kotlin.time.Clock
31 |
32 | val appModule = module {
33 |
34 | single { Clock.System } bind Clock::class
35 | singleOf(::SecureImageRepository)
36 | single { AppPreferencesDataSource(context = get()) }
37 | single {
38 | AuthorizationRepository(
39 | preferences = get(),
40 | pinRepository = get(),
41 | encryptionScheme = get(),
42 | context = get(),
43 | clock = get()
44 | )
45 | }
46 | singleOf(::LocationRepository)
47 | single {
48 | val detector = get()
49 | when (detector.detectSecurityLevel()) {
50 | SecurityLevel.SOFTWARE ->
51 | SoftwareEncryptionScheme(get())
52 | SecurityLevel.TEE, SecurityLevel.STRONGBOX -> {
53 | HardwareBackedEncryptionScheme(get(), get(), get())
54 | }
55 | }
56 | } bind EncryptionScheme::class
57 | single {
58 | val detector = get()
59 | when (detector.detectSecurityLevel()) {
60 | SecurityLevel.SOFTWARE ->
61 | PinRepositorySoftware(get(), get())
62 |
63 | SecurityLevel.TEE, SecurityLevel.STRONGBOX -> {
64 | PinRepositoryHardware(get(), get(), get())
65 | }
66 | }
67 | } bind PinRepository::class
68 | singleOf(::SecurityLevelDetector)
69 |
70 | single { WorkManager.getInstance(get()) }
71 |
72 | factoryOf(::DeviceInfo)
73 |
74 | factoryOf(::ThumbnailCache)
75 | factoryOf(::SecurityResetUseCase)
76 | factoryOf(::PinStrengthCheckUseCase)
77 | factoryOf(::VerifyPinUseCase)
78 | factoryOf(::CreatePinUseCase)
79 | factoryOf(::PinSizeUseCase)
80 | factoryOf(::RemovePoisonPillIUseCase)
81 | factoryOf(::MigratePinHash)
82 | factoryOf(::InvalidateSessionUseCase)
83 |
84 | viewModelOf(::ObfuscatePhotoViewModel)
85 | viewModelOf(::ViewPhotoViewModel)
86 | viewModelOf(::GalleryViewModel)
87 | viewModelOf(::SettingsViewModel)
88 | viewModelOf(::IntroductionViewModel)
89 | viewModelOf(::PinVerificationViewModel)
90 | viewModelOf(::ImportPhotosViewModel)
91 | }
92 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.darkrockstudios.app.securecamera.ui.UiEvent
6 | import kotlinx.coroutines.flow.*
7 | import kotlinx.coroutines.launch
8 |
9 | abstract class BaseViewModel : ViewModel() {
10 | protected val _uiState by lazy { MutableStateFlow(createState()) }
11 | val uiState: StateFlow by lazy { _uiState.asStateFlow() }
12 |
13 | protected abstract fun createState(): S
14 |
15 | private val _events = MutableSharedFlow()
16 | val events = _events.asSharedFlow()
17 |
18 | fun showMessage(message: String) {
19 | viewModelScope.launch {
20 | _events.emit(UiEvent.ShowSnack(message))
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/ConfirmDeletePhotoDialog.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import androidx.compose.material3.AlertDialog
4 | import androidx.compose.material3.Text
5 | import androidx.compose.material3.TextButton
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.res.stringResource
8 |
9 | @Composable
10 | fun ConfirmDeletePhotoDialog(
11 | selectedCount: Int,
12 | onConfirm: () -> Unit,
13 | onDismiss: () -> Unit,
14 | ) {
15 | AlertDialog(
16 | onDismissRequest = onDismiss,
17 | title = {
18 | Text(
19 | text = stringResource(
20 | id = if (selectedCount > 1) R.string.delete_photo_title_plural else R.string.delete_photo_title_singular
21 | )
22 | )
23 | },
24 | text = {
25 | Text(
26 | text = if (selectedCount > 1) {
27 | stringResource(id = R.string.delete_photo_message_plural, selectedCount)
28 | } else {
29 | stringResource(id = R.string.delete_photo_message_singular)
30 | }
31 | )
32 | },
33 | confirmButton = {
34 | TextButton(
35 | onClick = {
36 | onConfirm()
37 | }
38 | ) {
39 | Text(stringResource(id = R.string.delete_button))
40 | }
41 | },
42 | dismissButton = {
43 | TextButton(
44 | onClick = onDismiss
45 | ) {
46 | Text(stringResource(id = R.string.cancel_button))
47 | }
48 | }
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/ImageUtils.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import android.graphics.BitmapFactory
4 | import androidx.compose.ui.graphics.ImageBitmap
5 | import androidx.compose.ui.graphics.asImageBitmap
6 |
7 | /**
8 | * Extension function to convert a ByteArray to an ImageBitmap.
9 | * This is useful for displaying images captured from the camera in Jetpack Compose UI.
10 | *
11 | * @return ImageBitmap created from the ByteArray
12 | */
13 | fun ByteArray.decodeToImageBitmap(): ImageBitmap {
14 | // Convert ByteArray to Android Bitmap first
15 | val bitmap = BitmapFactory.decodeByteArray(this, 0, this.size)
16 |
17 | // Then convert Android Bitmap to Compose's ImageBitmap
18 | return bitmap.asImageBitmap()
19 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/KeepScreenOnEffect.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.compose.ui.platform.LocalView
6 |
7 | @Composable
8 | fun KeepScreenOnEffect() {
9 | val view = LocalView.current
10 |
11 | DisposableEffect(view) {
12 | view.keepScreenOn = true
13 | onDispose { view.keepScreenOn = false }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/ReentrantMutex.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import kotlinx.coroutines.Job
4 | import kotlinx.coroutines.currentCoroutineContext
5 | import kotlinx.coroutines.sync.Mutex
6 |
7 | class ReentrantMutex {
8 | private val mutex = Mutex()
9 | private var owner: Any? = null
10 | private var recursionCount = 0
11 |
12 | suspend fun lock() {
13 | val current = currentCoroutineContext()[Job]
14 | if (owner == current) {
15 | recursionCount++
16 | return
17 | }
18 | mutex.lock()
19 | owner = current
20 | recursionCount = 1
21 | }
22 |
23 | suspend fun unlock() {
24 | if (owner != currentCoroutineContext()[Job]) {
25 | throw IllegalStateException("Not the owner of the lock")
26 | }
27 | recursionCount--
28 | if (recursionCount == 0) {
29 | owner = null
30 | mutex.unlock()
31 | }
32 | }
33 |
34 | suspend fun withLock(action: suspend () -> T): T {
35 | lock()
36 | try {
37 | return action()
38 | } finally {
39 | unlock()
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/ReleaseLogTree.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import android.util.Log
4 | import timber.log.Timber
5 | import kotlin.math.min
6 |
7 |
8 | class ReleaseLogTree : Timber.Tree() {
9 | override fun isLoggable(tag: String?, priority: Int): Boolean {
10 | // Don't log VERBOSE or DEBUG
11 | if (priority == Log.VERBOSE || priority == Log.DEBUG) {
12 | return false
13 | }
14 |
15 | // Log only ERROR, WARN and WTF, INFO
16 | return true
17 | }
18 |
19 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
20 | if (isLoggable(tag, priority)) {
21 | // Message is short enough, doesn't need to be broken into chunks
22 |
23 | if (message.length < MAX_LOG_LENGTH) {
24 | if (priority == Log.ASSERT) {
25 | Log.wtf(tag, message)
26 | } else {
27 | Log.println(priority, tag, message)
28 | }
29 | return
30 | }
31 |
32 | // Split by line, then ensure each line can fit into Log's max length
33 | var i = 0
34 | val length = message.length
35 | while (i < length) {
36 | var newline = message.indexOf('\n', i)
37 | newline = if (newline != -1) newline else length
38 | do {
39 | val end = min(newline, i + MAX_LOG_LENGTH)
40 | val part = message.substring(i, end)
41 | if (priority == Log.ASSERT) {
42 | Log.wtf(tag, part)
43 | } else {
44 | Log.println(priority, tag, part)
45 | }
46 | i = end
47 | } while (i < newline)
48 | i++
49 | }
50 | }
51 | }
52 |
53 | companion object {
54 | private const val MAX_LOG_LENGTH = 4000
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/RequestLocationPermission.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import android.Manifest
4 | import android.content.pm.PackageManager
5 | import androidx.activity.compose.rememberLauncherForActivityResult
6 | import androidx.activity.result.contract.ActivityResultContracts
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.LaunchedEffect
9 | import androidx.compose.ui.platform.LocalContext
10 | import androidx.core.content.ContextCompat
11 |
12 | @Composable
13 | fun RequestLocationPermission(onGranted: () -> Unit) {
14 | val ctx = LocalContext.current
15 |
16 | val fineLauncher = rememberLauncherForActivityResult(
17 | contract = ActivityResultContracts.RequestPermission()
18 | ) { granted ->
19 | if (granted) {
20 | onGranted()
21 | } else {
22 | // If FINE location was denied, check if COARSE location is granted
23 | val coarsePerm = Manifest.permission.ACCESS_COARSE_LOCATION
24 | if (ContextCompat.checkSelfPermission(ctx, coarsePerm) == PackageManager.PERMISSION_GRANTED) {
25 | onGranted()
26 | }
27 | // We don't request COARSE if FINE was denied - as per requirements
28 | }
29 | }
30 |
31 | LaunchedEffect(Unit) {
32 | val finePerm = Manifest.permission.ACCESS_FINE_LOCATION
33 | val coarsePerm = Manifest.permission.ACCESS_COARSE_LOCATION
34 |
35 | // First check if FINE location is granted
36 | if (ContextCompat.checkSelfPermission(ctx, finePerm) == PackageManager.PERMISSION_GRANTED) {
37 | onGranted()
38 | }
39 | // Then check if COARSE location is granted
40 | else if (ContextCompat.checkSelfPermission(ctx, coarsePerm) == PackageManager.PERMISSION_GRANTED) {
41 | onGranted()
42 | }
43 | // If neither is granted, request FINE location
44 | else {
45 | fineLauncher.launch(finePerm)
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/SnapSafeApplication.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import android.app.Application
4 | import com.darkrockstudios.app.securecamera.camera.SecureImageRepository
5 | import com.darkrockstudios.app.securecamera.preferences.AppPreferencesDataSource
6 | import com.darkrockstudios.app.securecamera.usecases.SecurityResetUseCase
7 | import kotlinx.coroutines.flow.first
8 | import kotlinx.coroutines.runBlocking
9 | import org.koin.android.ext.koin.androidContext
10 | import org.koin.android.ext.koin.androidLogger
11 | import org.koin.core.component.KoinComponent
12 | import org.koin.core.component.inject
13 | import org.koin.core.context.startKoin
14 | import org.koin.core.module.Module
15 | import org.koin.dsl.module
16 | import timber.log.Timber
17 |
18 | open class SnapSafeApplication : Application(), KoinComponent {
19 | private val imageManager by inject()
20 | private val preferences by inject()
21 | private val securityReset by inject()
22 |
23 | override fun onCreate() {
24 | super.onCreate()
25 |
26 | if (BuildConfig.DEBUG) {
27 | Timber.plant(Timber.DebugTree())
28 | } else {
29 | Timber.plant(ReleaseLogTree())
30 | }
31 |
32 | startKoin {
33 | androidLogger()
34 | androidContext(this@SnapSafeApplication)
35 | modules(appModule, flavorModule())
36 | }
37 |
38 | handleMigrationFromBeta()
39 | }
40 |
41 | open fun flavorModule(): Module = module { }
42 |
43 | override fun onTrimMemory(level: Int) {
44 | super.onTrimMemory(level)
45 | imageManager.thumbnailCache.clear()
46 | }
47 |
48 | override fun onLowMemory() {
49 | super.onLowMemory()
50 | imageManager.thumbnailCache.clear()
51 | }
52 |
53 | // DELETE ME after a few versions
54 | private fun handleMigrationFromBeta() {
55 | runBlocking {
56 | val intoComplete = (preferences.hasCompletedIntro.first() == true)
57 | val isProdReady = (preferences.isProdReady.first() == true)
58 | val wasInBeta = intoComplete && !isProdReady
59 | if (wasInBeta) {
60 | securityReset.reset()
61 | preferences.markProdReady()
62 | } else if (!isProdReady) {
63 | preferences.markProdReady()
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/TimeUtils.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import timber.log.Timber
4 |
5 | /**
6 | * Measures the execution time of the provided block of code in milliseconds.
7 | * @param block The code block to measure.
8 | * @return A Pair containing the result of the block execution and the time taken in milliseconds.
9 | */
10 | inline fun measureTime(block: () -> T): Pair {
11 | val startTime = System.currentTimeMillis()
12 | val result = block()
13 | val endTime = System.currentTimeMillis()
14 | val executionTime = endTime - startTime
15 | return Pair(result, executionTime)
16 | }
17 |
18 | /**
19 | * Measures the execution time of the provided block of code, prints the time with a message,
20 | * and returns the result of the block execution.
21 | * @param message The message to print along with the measured time.
22 | * @param block The code block to measure.
23 | * @return The result of the block execution.
24 | */
25 | inline fun measureAndReport(message: String, block: () -> T): T {
26 | val (result, time) = measureTime(block)
27 | Timber.d("$message: $time ms")
28 | return result
29 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/Util.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import kotlinx.coroutines.runBlocking
4 | import kotlinx.coroutines.sync.Mutex
5 | import kotlinx.coroutines.sync.withLock
6 |
7 | inline fun Mutex.withLockBlocking(owner: Any? = null, crossinline action: () -> T): T {
8 | return runBlocking {
9 | withLock(owner, action)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/BottomCameraControls.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.camera
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.foundation.shape.CircleShape
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.Camera
10 | import androidx.compose.material.icons.filled.PhotoLibrary
11 | import androidx.compose.material.icons.filled.Settings
12 | import androidx.compose.material3.*
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.clip
17 | import androidx.compose.ui.platform.LocalContext
18 | import androidx.compose.ui.res.stringResource
19 | import androidx.compose.ui.semantics.contentDescription
20 | import androidx.compose.ui.semantics.semantics
21 | import androidx.compose.ui.unit.dp
22 | import androidx.navigation.NavHostController
23 | import com.darkrockstudios.app.securecamera.R
24 | import com.darkrockstudios.app.securecamera.navigation.AppDestinations
25 |
26 | @Composable
27 | fun BottomCameraControls(
28 | modifier: Modifier = Modifier,
29 | onCapture: (() -> Unit)?,
30 | isLoading: Boolean,
31 | navController: NavHostController,
32 | ) {
33 | val context = LocalContext.current
34 |
35 | Box(
36 | modifier = modifier
37 | .fillMaxWidth()
38 | .padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
39 | ) {
40 | ElevatedButton(
41 | onClick = { navController.navigate(AppDestinations.SETTINGS_ROUTE) },
42 | enabled = isLoading.not(),
43 | modifier = Modifier.align(Alignment.BottomStart),
44 | ) {
45 | Icon(
46 | imageVector = Icons.Filled.Settings,
47 | contentDescription = stringResource(R.string.camera_settings_button),
48 | modifier = Modifier.size(32.dp),
49 | )
50 | }
51 |
52 | if (onCapture != null) {
53 | FilledTonalButton(
54 | onClick = onCapture,
55 | modifier = Modifier
56 | .size(80.dp)
57 | .clip(CircleShape)
58 | .align(Alignment.BottomCenter)
59 | .semantics {
60 | contentDescription = context.getString(R.string.camera_shutter_button_desc)
61 | },
62 | colors = ButtonDefaults.filledTonalButtonColors(
63 | containerColor = MaterialTheme.colorScheme.primary,
64 | ),
65 | ) {
66 | Icon(
67 | imageVector = Icons.Filled.Camera,
68 | contentDescription = stringResource(id = R.string.camera_capture_content_description),
69 | tint = MaterialTheme.colorScheme.onPrimary,
70 | modifier = Modifier.size(32.dp),
71 | )
72 | }
73 | }
74 |
75 | ElevatedButton(
76 | onClick = { navController.navigate(AppDestinations.GALLERY_ROUTE) },
77 | enabled = isLoading.not(),
78 | modifier = Modifier.align(Alignment.BottomEnd),
79 | ) {
80 | Icon(
81 | imageVector = Icons.Filled.PhotoLibrary,
82 | contentDescription = stringResource(id = R.string.camera_gallery_content_description),
83 | modifier = Modifier.size(32.dp),
84 | )
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/CameraContent.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.camera
2 |
3 | import android.Manifest
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.runtime.*
8 | import androidx.compose.ui.Modifier
9 | import androidx.navigation.NavHostController
10 | import com.darkrockstudios.app.securecamera.KeepScreenOnEffect
11 | import com.google.accompanist.permissions.ExperimentalPermissionsApi
12 | import com.google.accompanist.permissions.rememberMultiplePermissionsState
13 |
14 | @OptIn(ExperimentalPermissionsApi::class)
15 | @Composable
16 | internal fun CameraContent(
17 | capturePhoto: MutableState,
18 | navController: NavHostController,
19 | modifier: Modifier,
20 | paddingValues: PaddingValues,
21 | ) {
22 | KeepScreenOnEffect()
23 |
24 | val permissionsState = rememberMultiplePermissionsState(
25 | permissions = listOf(
26 | Manifest.permission.CAMERA,
27 | )
28 | )
29 |
30 | var showRationaleDialog by remember { mutableStateOf(false) }
31 |
32 | LaunchedEffect(Unit) {
33 | if (!permissionsState.allPermissionsGranted && permissionsState.shouldShowRationale) {
34 | showRationaleDialog = true
35 | } else {
36 | permissionsState.launchMultiplePermissionRequest()
37 | }
38 | }
39 |
40 | if (showRationaleDialog) {
41 | CameraPermissionRationaleDialog(
42 | onContinue = {
43 | showRationaleDialog = false
44 | permissionsState.launchMultiplePermissionRequest()
45 | },
46 | onDismiss = { showRationaleDialog = false }
47 | )
48 | }
49 |
50 | Box(
51 | modifier = modifier
52 | .fillMaxSize()
53 | ) {
54 | if (permissionsState.allPermissionsGranted) {
55 | val cameraState = rememberCameraState()
56 | CameraPreview(
57 | modifier = Modifier.fillMaxSize(),
58 | state = cameraState
59 | )
60 |
61 | CameraControls(
62 | cameraController = cameraState,
63 | capturePhoto = capturePhoto,
64 | navController = navController,
65 | paddingValues = paddingValues,
66 | )
67 | } else {
68 | NoCameraPermission(navController, permissionsState)
69 | }
70 | }
71 | }
72 |
73 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/CameraPreview.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.camera
2 |
3 | import androidx.camera.core.CameraSelector
4 | import androidx.camera.lifecycle.ProcessCameraProvider
5 | import androidx.camera.view.PreviewView
6 | import androidx.compose.animation.core.animateFloatAsState
7 | import androidx.compose.animation.core.tween
8 | import androidx.compose.foundation.Canvas
9 | import androidx.compose.foundation.gestures.detectTapGestures
10 | import androidx.compose.foundation.gestures.rememberTransformableState
11 | import androidx.compose.foundation.gestures.transformable
12 | import androidx.compose.foundation.layout.Box
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.runtime.*
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.graphics.Color
17 | import androidx.compose.ui.input.pointer.pointerInput
18 | import androidx.compose.ui.platform.LocalContext
19 | import androidx.compose.ui.viewinterop.AndroidView
20 | import androidx.lifecycle.compose.LocalLifecycleOwner
21 | import kotlinx.coroutines.delay
22 | import kotlinx.coroutines.launch
23 |
24 | /** Create and remember a [CameraState] inside composition. */
25 | @Composable
26 | fun rememberCameraState(initialLensFacing: Int = CameraSelector.LENS_FACING_BACK): CameraState {
27 | val context = LocalContext.current
28 | val lifecycleOwner = LocalLifecycleOwner.current
29 |
30 | val provider = remember { ProcessCameraProvider.getInstance(context).get() }
31 | val previewView = remember {
32 | PreviewView(context).apply { scaleType = PreviewView.ScaleType.FIT_CENTER }
33 | }
34 |
35 | val state = remember {
36 | CameraState(
37 | previewView = previewView,
38 | lifecycleOwner = lifecycleOwner,
39 | providerFuture = provider,
40 | initialLensFacing = initialLensFacing
41 | )
42 | }
43 |
44 | // (Re)bind camera whenever lens changes.
45 | DisposableEffect(state.lensFacing) {
46 | state.bindCamera()
47 | onDispose { provider.unbindAll() }
48 | }
49 |
50 | DisposableEffect(Unit) {
51 | onDispose { state.shutdown() }
52 | }
53 |
54 | return state
55 | }
56 |
57 | /** Composable that renders the camera preview and UI using the provided [CameraState]. */
58 | @Composable
59 | fun CameraPreview(
60 | state: CameraState,
61 | modifier: Modifier = Modifier.fillMaxSize()
62 | ) {
63 | val scope = rememberCoroutineScope()
64 | // Pinch‑to‑zoom transformable
65 | val zoomState = rememberTransformableState { zoomChange, _, _ ->
66 | state.camera?.let { cam ->
67 | val current = cam.cameraInfo.zoomState.value?.zoomRatio ?: 1f
68 | val newZoom = (current * zoomChange).coerceIn(state.minZoom, state.maxZoom)
69 | state.setZoomRatio(newZoom)
70 | }
71 | }
72 |
73 | // focus indicator fade animation
74 | val indicatorAlpha by animateFloatAsState(
75 | targetValue = if (state.focusOffset != null) 1f else 0f,
76 | animationSpec = tween(durationMillis = 300)
77 | )
78 |
79 | Box(
80 | modifier = modifier
81 | .transformable(zoomState)
82 | .pointerInput(Unit) {
83 | detectTapGestures { offset ->
84 | state.focusAt(offset)
85 |
86 | scope.launch {
87 | delay(800)
88 | state.clearFocusOffset()
89 | }
90 | }
91 | }
92 | ) {
93 | // Camera preview
94 | AndroidView(
95 | factory = { state.previewView },
96 | modifier = Modifier.fillMaxSize()
97 | )
98 |
99 | // Draw focus ring
100 | state.focusOffset?.let { pos ->
101 | Canvas(
102 | modifier = Modifier
103 | .fillMaxSize()
104 | .pointerInput(Unit) {} // intercept taps
105 | ) {
106 | drawCircle(
107 | color = Color.White.copy(alpha = indicatorAlpha),
108 | radius = 40f,
109 | center = pos
110 | )
111 | }
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/CapturedImage.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.camera
2 |
3 | import android.graphics.Bitmap
4 | import kotlin.time.Instant
5 |
6 | data class CapturedImage(
7 | val sensorBitmap: Bitmap,
8 | val timestamp: Instant,
9 | val rotationDegrees: Int,
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/ImageUtils.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.camera
2 |
3 | import android.graphics.Bitmap
4 | import android.graphics.BitmapFactory
5 | import android.graphics.Matrix
6 | import androidx.camera.core.ImageProxy
7 | import java.io.ByteArrayOutputStream
8 | import java.nio.ByteBuffer
9 |
10 | internal fun Bitmap.rotate(degrees: Int): Bitmap {
11 | val m = Matrix();
12 | m.postRotate(degrees.toFloat())
13 | return Bitmap.createBitmap(this, 0, 0, width, height, m, true)
14 | }
15 |
16 | internal fun Bitmap.toJpegByteArray(quality: Int = 90): ByteArray {
17 | val out = ByteArrayOutputStream()
18 | compress(Bitmap.CompressFormat.JPEG, quality, out)
19 | return out.toByteArray()
20 | }
21 |
22 | internal fun imageProxyToBytes(proxy: ImageProxy): ByteArray {
23 | val buffer: ByteBuffer = proxy.planes[0].buffer
24 | return ByteArray(buffer.remaining()).also { buffer.get(it) }
25 | }
26 |
27 | internal fun rotateAndEncode(proxy: ImageProxy, quality: Int = 90): ByteArray {
28 | val bmp = BitmapFactory.decodeByteArray(imageProxyToBytes(proxy), 0, proxy.planes[0].buffer.remaining())
29 | val rotation = proxy.imageInfo.rotationDegrees
30 | val rotated = if (rotation == 0) bmp else bmp.rotate(rotation)
31 | return rotated.toJpegByteArray().also { if (rotated != bmp) bmp.recycle() }
32 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/NoCameraPermission.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.camera
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.provider.Settings
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.material3.Button
8 | import androidx.compose.material3.Card
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.platform.LocalContext
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.unit.dp
16 | import androidx.navigation.NavHostController
17 | import com.darkrockstudios.app.securecamera.R
18 | import com.google.accompanist.permissions.ExperimentalPermissionsApi
19 | import com.google.accompanist.permissions.MultiplePermissionsState
20 |
21 | @OptIn(ExperimentalPermissionsApi::class)
22 | @Composable
23 | fun NoCameraPermission(
24 | navController: NavHostController,
25 | permissionsState: MultiplePermissionsState,
26 | ) {
27 | val context = LocalContext.current
28 |
29 | Box(
30 | modifier = Modifier
31 | .fillMaxSize()
32 | ) {
33 | Card(modifier = Modifier.align(Alignment.Center)) {
34 | Column(
35 | modifier = Modifier.padding(16.dp),
36 | horizontalAlignment = Alignment.CenterHorizontally,
37 | verticalArrangement = Arrangement.Center,
38 | ) {
39 | Text(
40 | stringResource(R.string.camera_permissions_required),
41 | modifier = Modifier.padding(16.dp)
42 | )
43 |
44 | if (permissionsState.revokedPermissions.isNotEmpty()) {
45 | Button(onClick = {
46 | val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
47 | data = Uri.fromParts("package", context.packageName, null)
48 | }
49 | context.startActivity(intent)
50 | }) {
51 | Text(text = stringResource(R.string.camera_open_settings))
52 | }
53 | }
54 | }
55 | }
56 |
57 | BottomCameraControls(
58 | modifier = Modifier.align(Alignment.BottomCenter),
59 | navController = navController,
60 | onCapture = null,
61 | isLoading = false,
62 | )
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/PermissionRationaleDialog.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.camera
2 |
3 | import androidx.compose.material.icons.Icons
4 | import androidx.compose.material.icons.filled.CameraAlt
5 | import androidx.compose.material3.AlertDialog
6 | import androidx.compose.material3.Icon
7 | import androidx.compose.material3.Text
8 | import androidx.compose.material3.TextButton
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.graphics.vector.ImageVector
11 |
12 | @Composable
13 | fun CameraPermissionRationaleDialog(
14 | onContinue: () -> Unit,
15 | onDismiss: () -> Unit,
16 | title: String = "Camera access required",
17 | text: String =
18 | "We use the camera to capture photos. Images stay on your device unless you choose to share them.\n\n" +
19 | "We use location to save location of each photo when it's taken. This metadata is automatically stripped out by default when sharing the photos.\n\n" +
20 | "Denying Location permissions is okay, Location metadata will just not be attached to your photos.",
21 |
22 | icon: ImageVector = Icons.Default.CameraAlt
23 | ) {
24 | AlertDialog(
25 | icon = { Icon(icon, contentDescription = null) },
26 | title = { Text(title) },
27 | text = { Text(text) },
28 | onDismissRequest = onDismiss,
29 | confirmButton = {
30 | TextButton(onClick = onContinue) { Text("Request Permissions") }
31 | },
32 | dismissButton = {
33 | TextButton(onClick = onDismiss) { Text("Not now") }
34 | }
35 | )
36 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/PhotoDef.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.camera
2 |
3 | import timber.log.Timber
4 | import java.io.File
5 | import java.text.ParseException
6 | import java.text.SimpleDateFormat
7 | import java.util.*
8 |
9 | data class PhotoDef(
10 | val photoName: String,
11 | val photoFormat: String,
12 | val photoFile: File,
13 | ) {
14 | fun dateTaken(): Date {
15 | try {
16 | val dateString = photoName.removePrefix("photo_").removeSuffix(".jpg")
17 | val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss_SS", Locale.US)
18 | return dateFormat.parse(dateString) ?: Date()
19 | } catch (e: ParseException) {
20 | Timber.w(e, "Failed to parse photo name to date")
21 | return Date()
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/PhotoMetaData.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.camera
2 |
3 | import android.util.Size
4 | import com.ashampoo.kim.model.GpsCoordinates
5 | import com.ashampoo.kim.model.TiffOrientation
6 | import java.util.*
7 |
8 | data class PhotoMetaData(
9 | val name: String,
10 | val resolution: Size,
11 | val dateTaken: Date,
12 | val orientation: TiffOrientation?,
13 | val location: GpsCoordinates?,
14 | ) {
15 | fun resolutionString() = "${resolution.width}x${resolution.height}"
16 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/ThumbnailCache.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.camera
2 |
3 | import android.graphics.Bitmap
4 | import android.util.LruCache
5 | import com.darkrockstudios.app.securecamera.withLockBlocking
6 | import kotlinx.coroutines.sync.Mutex
7 | import kotlinx.coroutines.sync.withLock
8 |
9 | class ThumbnailCache {
10 | private val maxMemory = Runtime.getRuntime().maxMemory() / 1024
11 | private val cacheSize = (maxMemory / 8).toInt()
12 |
13 | private val thumbnailCache = object : LruCache(cacheSize) {
14 | override fun sizeOf(key: String, bitmap: Bitmap): Int {
15 | // The cache size will be measured in kilobytes
16 | return bitmap.byteCount / 1024
17 | }
18 | }
19 | private val cacheMutex = Mutex()
20 |
21 | suspend fun getThumbnail(photo: PhotoDef): Bitmap? {
22 | return cacheMutex.withLock {
23 | thumbnailCache.get(photo.photoName)
24 | }
25 | }
26 |
27 | fun evictThumbnail(photo: PhotoDef) {
28 | cacheMutex.withLockBlocking {
29 | thumbnailCache.remove(photo.photoName)
30 | }
31 | }
32 |
33 | fun clear() {
34 | cacheMutex.withLockBlocking {
35 | thumbnailCache.evictAll()
36 | }
37 | }
38 |
39 | suspend fun putThumbnail(photo: PhotoDef, thumbnailBitmap: Bitmap) {
40 | cacheMutex.withLock {
41 | thumbnailCache.put(photo.photoName, thumbnailBitmap)
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/tiffOrientation.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.camera
2 |
3 | import com.ashampoo.kim.model.TiffOrientation
4 |
5 | /**
6 | * Pure logic: convert a rotation (0/90/180/270 deg) and an optional mirror flag
7 | * into the correct TIFF orientation value.
8 | */
9 | fun calculateTiffOrientation(
10 | rotationDegrees: Int,
11 | isMirrored: Boolean = false
12 | ): TiffOrientation = when (rotationDegrees) {
13 | 0 -> if (isMirrored) TiffOrientation.MIRROR_HORIZONTAL
14 | else TiffOrientation.STANDARD
15 |
16 | 90 -> if (isMirrored) TiffOrientation.MIRROR_HORIZONTAL_AND_ROTATE_LEFT
17 | else TiffOrientation.ROTATE_RIGHT
18 |
19 | 180 -> if (isMirrored) TiffOrientation.MIRROR_VERTICAL
20 | else TiffOrientation.UPSIDE_DOWN
21 |
22 | 270 -> if (isMirrored) TiffOrientation.MIRROR_HORIZONTAL_AND_ROTATE_RIGHT
23 | else TiffOrientation.ROTATE_LEFT
24 |
25 | else -> TiffOrientation.STANDARD // Fallback
26 | }
27 |
28 | fun TiffOrientation.toDegrees(): Int = when (this) {
29 | TiffOrientation.STANDARD,
30 | TiffOrientation.MIRROR_HORIZONTAL -> 0
31 |
32 | TiffOrientation.ROTATE_RIGHT,
33 | TiffOrientation.MIRROR_HORIZONTAL_AND_ROTATE_LEFT -> 90
34 |
35 | TiffOrientation.UPSIDE_DOWN,
36 | TiffOrientation.MIRROR_VERTICAL -> 180
37 |
38 | TiffOrientation.ROTATE_LEFT,
39 | TiffOrientation.MIRROR_HORIZONTAL_AND_ROTATE_RIGHT -> 270
40 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/gallery/rememberPhotoPickerLauncher.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.gallery
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.net.Uri
6 | import androidx.activity.compose.rememberLauncherForActivityResult
7 | import androidx.activity.result.contract.ActivityResultContracts
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.platform.LocalContext
10 | import com.darkrockstudios.app.securecamera.R
11 |
12 | @Composable
13 | fun rememberPhotoPickerLauncher(onImagesSelected: (List) -> Unit): () -> Unit {
14 | val context = LocalContext.current
15 | val pickImagesLauncher = rememberLauncherForActivityResult(
16 | contract = ActivityResultContracts.StartActivityForResult()
17 | ) { result ->
18 | if (result.resultCode == Activity.RESULT_OK) {
19 | val selectedUris = mutableListOf()
20 | val data = result.data
21 |
22 | // Handle single image
23 | val singleData = data?.data
24 | if (singleData != null) {
25 | selectedUris.add(singleData)
26 | }
27 | // Handle multiple images
28 | else if (data?.clipData != null) {
29 | val clipData = data.clipData!!
30 | for (i in 0 until clipData.itemCount) {
31 | selectedUris.add(clipData.getItemAt(i).uri)
32 | }
33 | }
34 |
35 | if (selectedUris.isNotEmpty()) {
36 | onImagesSelected(selectedUris)
37 | }
38 | }
39 | }
40 |
41 | return {
42 | val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
43 | type = "image/jpeg"
44 | putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
45 | }
46 | pickImagesLauncher.launch(
47 | Intent.createChooser(intent, context.getString(R.string.import_chooser_intent_title))
48 | )
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/gallery/vibrateDevice.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.gallery
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import android.os.VibrationEffect
6 | import android.os.Vibrator
7 | import android.os.VibratorManager
8 |
9 | fun vibrateDevice(context: Context) {
10 | val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
11 | val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
12 | vibratorManager.defaultVibrator
13 | } else {
14 | @Suppress("DEPRECATION")
15 | context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
16 | }
17 |
18 | vibrator.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE))
19 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/import/ImportCancelReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.import
2 |
3 | import android.app.NotificationManager
4 | import android.content.BroadcastReceiver
5 | import android.content.Context
6 | import android.content.Intent
7 | import androidx.work.WorkManager
8 | import com.darkrockstudios.app.securecamera.import.ImportWorker.Companion.NOTIFICATION_ID
9 | import timber.log.Timber
10 |
11 | /**
12 | * BroadcastReceiver to handle cancellation of the ImportWorker
13 | */
14 | class ImportCancelReceiver : BroadcastReceiver() {
15 | companion object {
16 | const val ACTION_CANCEL_IMPORT = "com.darkrockstudios.app.securecamera.import.CANCEL_IMPORT"
17 | const val EXTRA_WORKER_ID = "EXTRA_WORKER_ID"
18 | }
19 |
20 | override fun onReceive(context: Context, intent: Intent) {
21 | if (intent.action == ACTION_CANCEL_IMPORT) {
22 | val workerId = intent.getStringExtra(EXTRA_WORKER_ID)
23 | if (workerId != null) {
24 | Timber.d("Cancelling import worker: $workerId")
25 | WorkManager.getInstance(context).cancelWorkById(workerId.toUUID())
26 |
27 | // Dismiss notification when worker is cancelled
28 | val notificationManager =
29 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
30 | notificationManager.cancel(NOTIFICATION_ID)
31 | } else {
32 | Timber.e("No worker ID provided in cancel intent")
33 | }
34 | }
35 | }
36 |
37 | private fun String.toUUID() = java.util.UUID.fromString(this)
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/import/ImportPhotosTopBar.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.import
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.Close
6 | import androidx.compose.material3.ExperimentalMaterial3Api
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.IconButton
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Text
11 | import androidx.compose.material3.TopAppBar
12 | import androidx.compose.material3.TopAppBarDefaults
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.unit.dp
17 | import androidx.navigation.NavController
18 | import com.darkrockstudios.app.securecamera.R
19 |
20 | @OptIn(ExperimentalMaterial3Api::class)
21 | @Composable
22 | fun ImportPhotosTopBar(
23 | navController: NavController,
24 | ) {
25 | TopAppBar(
26 | title = {
27 | Text(
28 | stringResource(id = R.string.import_photos_title),
29 | color = MaterialTheme.colorScheme.onPrimaryContainer,
30 | )
31 | },
32 | colors = TopAppBarDefaults.topAppBarColors(
33 | containerColor = MaterialTheme.colorScheme.primaryContainer,
34 | titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
35 | ),
36 | navigationIcon = {
37 | IconButton(
38 | onClick = { navController.navigateUp() },
39 | modifier = Modifier.padding(8.dp)
40 | ) {
41 | Icon(
42 | imageVector = Icons.Filled.Close,
43 | contentDescription = stringResource(id = R.string.close_import_photos_content_description),
44 | tint = MaterialTheme.colorScheme.onPrimaryContainer,
45 | )
46 | }
47 | }
48 | )
49 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/introduction/IntroductionSlide.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.introduction
2 |
3 | import androidx.compose.ui.graphics.vector.ImageVector
4 |
5 | /**
6 | * Represents a slide in the introduction
7 | */
8 | data class IntroductionSlide(
9 | val icon: ImageVector,
10 | val title: String,
11 | val description: String,
12 | )
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/navigation/NavAnimations.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.navigation
2 |
3 | import androidx.compose.animation.*
4 | import androidx.compose.animation.core.CubicBezierEasing
5 | import androidx.compose.animation.core.tween
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.unit.IntOffset
8 | import androidx.navigation.NamedNavArgument
9 | import androidx.navigation.NavBackStackEntry
10 | import androidx.navigation.NavDeepLink
11 | import androidx.navigation.compose.composable
12 |
13 | private const val Duration = 240
14 | private val StandardEasing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f)
15 |
16 | private val fSpec = tween(
17 | durationMillis = Duration,
18 | easing = StandardEasing
19 | )
20 |
21 | private val iSpec = tween(
22 | durationMillis = Duration,
23 | easing = StandardEasing
24 | )
25 |
26 | fun defaultEnter(): EnterTransition =
27 | fadeIn(animationSpec = fSpec) +
28 | scaleIn(
29 | // start a little smaller – matches Settings/Files app
30 | initialScale = 0.75f,
31 | animationSpec = fSpec
32 | ) +
33 | slideInVertically(
34 | // slide up ≈10 % of the height
35 | initialOffsetY = { it / 10 },
36 | animationSpec = iSpec
37 | )
38 |
39 | fun defaultExit(): ExitTransition =
40 | fadeOut(animationSpec = fSpec) +
41 | scaleOut(
42 | targetScale = 0.9f, // recedes slightly
43 | animationSpec = fSpec
44 | ) +
45 | slideOutVertically(
46 | targetOffsetY = { -it / 10 }, // moves up a bit
47 | animationSpec = iSpec
48 | )
49 |
50 | fun defaultPopEnter(): EnterTransition =
51 | fadeIn(animationSpec = fSpec) +
52 | scaleIn(
53 | initialScale = 0.9f, // small lift-off, then settles
54 | animationSpec = fSpec
55 | ) +
56 | slideInVertically(
57 | initialOffsetY = { -it / 10 }, // comes down slightly
58 | animationSpec = iSpec
59 | )
60 |
61 | fun defaultPopExit(): ExitTransition =
62 | fadeOut(animationSpec = fSpec) +
63 | scaleOut(
64 | targetScale = 0.75f,
65 | animationSpec = fSpec
66 | ) +
67 | slideOutVertically(
68 | targetOffsetY = { it / 10 }, // slides downward
69 | animationSpec = iSpec
70 | )
71 |
72 | fun androidx.navigation.NavGraphBuilder.defaultAnimatedComposable(
73 | route: String,
74 | arguments: List = emptyList(),
75 | deepLinks: List = emptyList(),
76 | content: @Composable() (AnimatedContentScope.(NavBackStackEntry) -> Unit)
77 | ) {
78 | composable(
79 | route = route,
80 | arguments = arguments,
81 | deepLinks = deepLinks,
82 | enterTransition = { defaultEnter() },
83 | exitTransition = { defaultExit() },
84 | popEnterTransition = { defaultPopEnter() },
85 | popExitTransition = { defaultPopExit() },
86 | content = content,
87 | )
88 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/obfuscation/FacialDetection.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.obfuscation
2 |
3 | import android.graphics.Bitmap
4 | import android.graphics.PointF
5 | import android.graphics.Rect
6 |
7 | interface FacialDetection {
8 | suspend fun processForFaces(bitmap: Bitmap): List
9 |
10 | data class FoundFace(
11 | val boundingBox: Rect,
12 | val eyes: Eyes?
13 | ) {
14 | data class Eyes(
15 | val left: PointF,
16 | val right: PointF,
17 | )
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/obfuscation/ObfuscatePhotoTopBar.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.obfuscation
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.foundation.layout.size
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.filled.AddBox
7 | import androidx.compose.material.icons.filled.ArrowBack
8 | import androidx.compose.material3.*
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.unit.dp
13 | import com.darkrockstudios.app.securecamera.R
14 |
15 | @OptIn(ExperimentalMaterial3Api::class)
16 | @Composable
17 | fun ObfuscatePhotoTopBar(
18 | onAddRegionClick: () -> Unit,
19 | isFindingFaces: Boolean,
20 | isCreatingRegion: Boolean = false,
21 | onBackPressed: () -> Unit,
22 | ) {
23 | TopAppBar(
24 | title = {
25 | Text(
26 | stringResource(id = R.string.obfuscate_photo_button),
27 | color = MaterialTheme.colorScheme.onPrimaryContainer,
28 | )
29 | },
30 | colors = TopAppBarDefaults.topAppBarColors(
31 | containerColor = MaterialTheme.colorScheme.primaryContainer,
32 | titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
33 | ),
34 | navigationIcon = {
35 | IconButton(
36 | onClick = { onBackPressed() },
37 | modifier = Modifier.padding(8.dp)
38 | ) {
39 | Icon(
40 | imageVector = Icons.Filled.ArrowBack,
41 | contentDescription = stringResource(id = R.string.settings_back_description),
42 | tint = MaterialTheme.colorScheme.onPrimaryContainer,
43 | )
44 | }
45 | },
46 | actions = {
47 | if (!isCreatingRegion) {
48 | if (isFindingFaces) {
49 | CircularProgressIndicator(
50 | modifier = Modifier
51 | .padding(8.dp)
52 | .size(24.dp),
53 | color = MaterialTheme.colorScheme.onPrimaryContainer,
54 | strokeWidth = 2.dp
55 | )
56 | } else {
57 | // Add Region button
58 | IconButton(
59 | onClick = onAddRegionClick,
60 | modifier = Modifier.padding(8.dp),
61 | ) {
62 | Icon(
63 | imageVector = Icons.Filled.AddBox,
64 | contentDescription = stringResource(id = R.string.obscure_action_button_add_region),
65 | )
66 | }
67 | }
68 | }
69 | }
70 | )
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/obfuscation/Region.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.obfuscation
2 |
3 | import android.graphics.Rect
4 |
5 | sealed class Region(
6 | val rect: Rect,
7 | val obfuscate: Boolean = true,
8 | )
9 |
10 | class FaceRegion(
11 | val face: FacialDetection.FoundFace,
12 | obfuscate: Boolean = true,
13 | ) : Region(face.boundingBox, obfuscate)
14 |
15 | class ManualRegion(
16 | rect: Rect,
17 | obfuscate: Boolean = true,
18 | ) : Region(rect, obfuscate)
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/preferences/XorCipher.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.preferences
2 |
3 | import java.nio.charset.StandardCharsets
4 | import kotlin.io.encoding.Base64
5 | import kotlin.io.encoding.ExperimentalEncodingApi
6 |
7 |
8 | object XorCipher {
9 | @OptIn(ExperimentalEncodingApi::class)
10 | fun encrypt(plaintext: String, key: String): String {
11 | if (key.isBlank()) error("Key must not be empty!")
12 | val obfuscated = xor(plaintext.toByteArray(StandardCharsets.UTF_8), key)
13 | return String(Base64.encodeToByteArray(obfuscated))
14 | }
15 |
16 | @OptIn(ExperimentalEncodingApi::class)
17 | fun decrypt(ciphertextB64: String, key: String): String {
18 | if (key.isBlank()) error("Key must not be empty!")
19 | val decoded: ByteArray = Base64.decode(ciphertextB64)
20 | return String(xor(decoded, key), StandardCharsets.UTF_8)
21 | }
22 |
23 | private fun xor(input: ByteArray, key: String): ByteArray {
24 | val key = key.toByteArray(StandardCharsets.UTF_8)
25 | val out = ByteArray(input.size)
26 |
27 | for (i in input.indices) {
28 | out[i] = (input[i].toInt() xor key[i % key.size].toInt()).toByte()
29 | }
30 | return out
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/security/KeyParams.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.security
2 |
3 | import dev.whyoleg.cryptography.BinarySize
4 | import dev.whyoleg.cryptography.BinarySize.Companion.bytes
5 |
6 | data class KeyParams(
7 | val iterations: Int = 600_000,
8 | val outputSize: BinarySize = 32.bytes,
9 | )
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/security/SchemeConfig.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.security
2 |
3 | import kotlinx.serialization.Serializable
4 | import kotlin.time.Duration
5 |
6 | @Serializable
7 | sealed class SchemeConfig
8 |
9 | @Serializable
10 | data object SoftwareSchemeConfig : SchemeConfig()
11 |
12 | @Serializable
13 | data class HardwareSchemeConfig(
14 | val requireBiometricAttestation: Boolean,
15 | val authTimeout: Duration,
16 | val ephemeralKey: Boolean
17 | ) : SchemeConfig()
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/security/SharedKey.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.security
2 |
3 | import dev.whyoleg.cryptography.random.CryptographyRandom
4 | import java.nio.ByteBuffer
5 | import kotlin.random.Random
6 |
7 | class ShardedKey(
8 | key: ByteArray
9 | ) {
10 | private val part1: ByteBuffer
11 | private val part2: ByteBuffer
12 | private val keySize: Int = key.size
13 |
14 | init {
15 | // Randomly size the storage buffer so it is not the exact length of an AES key
16 | val part1Size = keySize + CryptographyRandom.nextInt(3, 155)
17 | part1 = ByteBuffer.allocateDirect(part1Size)
18 | val part1Array = ByteArray(part1Size)
19 | CryptographyRandom.nextBytes(part1Array)
20 | part1.put(part1Array)
21 | part1.flip()
22 |
23 | // Best effort to ensure the two key parts don't live next to each other in memory.
24 | // This will be released once the constructor returns (or when ever the GC runs next)
25 | val spacerSize = Random.nextInt(978, 2893)
26 | val spacer = ByteBuffer.allocateDirect(spacerSize)
27 | val spacerArray = ByteArray(spacerSize)
28 | CryptographyRandom.nextBytes(spacerArray)
29 | spacer.put(spacerArray)
30 | spacer.flip()
31 |
32 | // Use it so it doesn't get optimized out
33 | (0..Random.nextInt(13)).forEachIndexed { i, _ ->
34 | val x = spacer.get(0) + spacer.get(i)
35 | // Almost never true, but compiler doesn't know that
36 | if (System.nanoTime() % 10000 == 0L) {
37 | // Side effect that's rarely executed
38 | println("$x")
39 | println(System.identityHashCode(spacer))
40 | }
41 | }
42 |
43 | // Randomly size the storage buffer so it is not the exact length of an AES key
44 | val part2Size = keySize + CryptographyRandom.nextInt(5, 111)
45 | part2 = ByteBuffer.allocateDirect(part2Size)
46 | val part2Array = ByteArray(part2Size)
47 | CryptographyRandom.nextBytes(part2Array)
48 | part2.put(part2Array)
49 | part2.flip()
50 |
51 | // Now fill the data part with our XOR'd key
52 | for (i in key.indices) {
53 | val xorByte = (key[i].toInt() xor part1.get(i).toInt()).toByte()
54 | part2.put(i, xorByte)
55 | }
56 | }
57 |
58 | /**
59 | * Reconstructs the original key from its XOR-split parts.
60 | *
61 | * @return The reconstructed original key
62 | */
63 | fun reconstructKey(): ByteArray {
64 | val originalKey = ByteArray(keySize)
65 | for (i in originalKey.indices) {
66 | originalKey[i] = (part1.get(i).toInt() xor part2.get(i).toInt()).toByte()
67 | }
68 |
69 | return originalKey
70 | }
71 |
72 | fun evict() {
73 | part1.zeroOut()
74 | part2.zeroOut()
75 | }
76 | }
77 |
78 | private fun ByteBuffer.zeroOut() {
79 | if (hasArray()) {
80 | val a = array()
81 | val start = arrayOffset() + position()
82 | val end = arrayOffset() + limit()
83 | java.util.Arrays.fill(a, start, end, 0.toByte())
84 | } else {
85 | val pos = position()
86 | val lim = limit()
87 | for (i in 0 until capacity()) {
88 | put(i, 0)
89 | }
90 | position(pos)
91 | limit(lim)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/security/deviceInfo.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.security
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.provider.Settings
6 | import dev.whyoleg.cryptography.CryptographyProvider
7 | import dev.whyoleg.cryptography.algorithms.SHA512
8 | import dev.whyoleg.cryptography.operations.Hasher
9 | import kotlin.io.encoding.ExperimentalEncodingApi
10 |
11 | class DeviceInfo(private val appContext: Context) {
12 | private val hasher: Hasher = CryptographyProvider.Default.get(SHA512).hasher()
13 |
14 | @OptIn(ExperimentalStdlibApi::class, ExperimentalEncodingApi::class)
15 | @SuppressLint("HardwareIds")
16 | suspend fun getDeviceIdentifier(): ByteArray {
17 | val androidId = Settings.Secure.getString(
18 | appContext.contentResolver,
19 | Settings.Secure.ANDROID_ID
20 | )
21 | val id = androidId + android.os.Build.MANUFACTURER + android.os.Build.MODEL
22 | val hashed = hasher.hash(id.toByteArray())
23 | return hashed
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/security/pin/PinRepository.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.security.pin
2 |
3 | import com.darkrockstudios.app.securecamera.preferences.HashedPin
4 | import com.darkrockstudios.app.securecamera.security.SchemeConfig
5 |
6 | interface PinRepository {
7 | suspend fun setAppPin(pin: String, schemeConfig: SchemeConfig)
8 | suspend fun getHashedPin(): HashedPin?
9 |
10 | suspend fun verifySecurityPin(pin: String): Boolean {
11 | val storedHashedPin = getHashedPin() ?: return false
12 | return verifyPin(pin, storedHashedPin)
13 | }
14 |
15 | suspend fun hashPin(pin: String): HashedPin
16 | suspend fun verifyPin(inputPin: String, storedHash: HashedPin): Boolean
17 | suspend fun setPoisonPillPin(pin: String)
18 | suspend fun getPlainPoisonPillPin(): String?
19 | suspend fun getHashedPoisonPillPin(): HashedPin?
20 | suspend fun activatePoisonPill()
21 | suspend fun removePoisonPillPin()
22 |
23 | /**
24 | * Check if a Poison Pill PIN is set
25 | */
26 | suspend fun hasPoisonPillPin(): Boolean {
27 | return getHashedPin() != null && getHashedPoisonPillPin() != null
28 | }
29 |
30 | /**
31 | * Verify if the input PIN matches the Poison Pill PIN
32 | */
33 | suspend fun verifyPoisonPillPin(pin: String): Boolean {
34 | val storedHashedPin = getHashedPoisonPillPin() ?: return false
35 | return verifyPin(pin, storedHashedPin)
36 | }
37 |
38 | companion object {
39 | const val ARGON_ITERATIONS = 5
40 | const val ARGON_COST = 65536
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/security/schemes/EncryptionScheme.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.security.schemes
2 |
3 | import com.darkrockstudios.app.securecamera.preferences.HashedPin
4 | import java.io.File
5 |
6 | /**
7 | * Encryption schemes used to encrypt and decrypt files.
8 | * We have two concrete Schemes: Software and Hardware
9 | */
10 | interface EncryptionScheme {
11 | /**
12 | * Encrypts plaintext data and writes it to a file.
13 | * This will use the pre-derived key in the cache.
14 | */
15 | suspend fun encryptToFile(plain: ByteArray, targetFile: File)
16 |
17 | /**
18 | * Encrypts plaintext data using the provided key and writes it to a file
19 | */
20 | suspend fun encryptToFile(plain: ByteArray, keyBytes: ByteArray, targetFile: File)
21 |
22 | /**
23 | * Encrypts plaintext data using the provided key and returns the ciphered bytes
24 | */
25 | suspend fun encrypt(plain: ByteArray, keyBytes: ByteArray): ByteArray
26 |
27 | /**
28 | * Encrypts plaintext data using the provided key alias and returns the ciphered bytes
29 | */
30 | suspend fun encryptWithKeyAlias(plain: ByteArray, keyAlias: String): ByteArray
31 |
32 | /**
33 | * Decrypts ciphered data using the provided key alias and returns the plain text bytes
34 | */
35 | suspend fun decryptWithKeyAlias(encrypted: ByteArray, keyAlias: String): ByteArray
36 |
37 | /**
38 | * Decrypts an encrypted file and returns the plaintext data
39 | * This will use the pre-derived key in memory.
40 | */
41 | suspend fun decryptFile(encryptedFile: File): ByteArray
42 |
43 | suspend fun deriveAndCacheKey(plainPin: String, hashedPin: HashedPin)
44 |
45 | /**
46 | * Get the currently derived encryption key,
47 | * or throws if one hasn't been derived yet.
48 | */
49 | suspend fun getDerivedKey(): ByteArray
50 |
51 | suspend fun deriveKey(plainPin: String, hashedPin: HashedPin): ByteArray
52 |
53 | fun evictKey()
54 |
55 | /**
56 | * First time key creation
57 | */
58 | suspend fun createKey(plainPin: String, hashedPin: HashedPin)
59 |
60 | suspend fun securityFailureReset()
61 |
62 | fun activatePoisonPill(oldPin: HashedPin?)
63 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/settings/DecoyPhotoExplanationDialog.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.settings
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.foundation.rememberScrollState
8 | import androidx.compose.foundation.verticalScroll
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.filled.Photo
11 | import androidx.compose.material3.*
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.text.style.TextAlign
17 | import androidx.compose.ui.unit.dp
18 | import androidx.compose.ui.window.Dialog
19 | import com.darkrockstudios.app.securecamera.R
20 |
21 | @Composable
22 | fun DecoyPhotoExplanationDialog(
23 | onDismiss: () -> Unit
24 | ) {
25 | 1337
26 | Dialog(onDismissRequest = onDismiss) {
27 | Card(
28 | modifier = Modifier
29 | .fillMaxWidth()
30 | .padding(16.dp),
31 | shape = MaterialTheme.shapes.medium
32 | ) {
33 | Column(
34 | modifier = Modifier
35 | .fillMaxWidth()
36 | .verticalScroll(rememberScrollState())
37 | .padding(16.dp),
38 | horizontalAlignment = Alignment.CenterHorizontally
39 | ) {
40 | Icon(
41 | modifier = Modifier
42 | .size(64.dp)
43 | .padding(8.dp),
44 | imageVector = Icons.Filled.Photo,
45 | contentDescription = null,
46 | tint = MaterialTheme.colorScheme.primary
47 | )
48 |
49 | Text(
50 | text = stringResource(R.string.decoy_explanation_dialog_title),
51 | style = MaterialTheme.typography.headlineSmall,
52 | textAlign = TextAlign.Center,
53 | modifier = Modifier.padding(bottom = 8.dp)
54 | )
55 |
56 | Text(
57 | text = stringResource(R.string.decoy_explanation_dialog_message),
58 | style = MaterialTheme.typography.bodyMedium,
59 | modifier = Modifier.padding(bottom = 16.dp)
60 | )
61 |
62 | // OK button
63 | Button(
64 | onClick = onDismiss,
65 | modifier = Modifier
66 | .fillMaxWidth()
67 | .padding(top = 8.dp)
68 | ) {
69 | Text(stringResource(R.string.decoy_explanation_dialog_ok_button))
70 | }
71 | }
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/settings/LocationDialog.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.settings
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.provider.Settings
7 | import androidx.compose.foundation.layout.*
8 | import androidx.compose.material3.*
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.unit.dp
13 | import androidx.compose.ui.window.Dialog
14 | import com.darkrockstudios.app.securecamera.LocationPermissionStatus
15 | import com.darkrockstudios.app.securecamera.R
16 |
17 | @Composable
18 | fun LocationDialog(
19 | locationPermissionStatus: LocationPermissionStatus,
20 | context: Context,
21 | onDismiss: () -> Unit
22 | ) {
23 | Dialog(onDismissRequest = onDismiss) {
24 | Card(
25 | modifier = Modifier.Companion
26 | .fillMaxWidth()
27 | .padding(16.dp),
28 | shape = MaterialTheme.shapes.medium
29 | ) {
30 | Column(
31 | modifier = Modifier.Companion
32 | .fillMaxWidth()
33 | .padding(16.dp)
34 | ) {
35 | Text(
36 | text = when (locationPermissionStatus) {
37 | LocationPermissionStatus.DENIED -> stringResource(id = R.string.location_dialog_title_denied)
38 | LocationPermissionStatus.COARSE -> stringResource(id = R.string.location_dialog_title_coarse)
39 | LocationPermissionStatus.FINE -> stringResource(id = R.string.location_dialog_title_fine)
40 | },
41 | style = MaterialTheme.typography.headlineSmall
42 | )
43 |
44 | Spacer(modifier = Modifier.Companion.height(8.dp))
45 |
46 | Text(
47 | text = when (locationPermissionStatus) {
48 | LocationPermissionStatus.DENIED -> stringResource(id = R.string.location_dialog_message_denied)
49 | LocationPermissionStatus.COARSE -> stringResource(id = R.string.location_dialog_message_coarse)
50 | LocationPermissionStatus.FINE -> stringResource(id = R.string.location_dialog_message_fine)
51 | },
52 | style = MaterialTheme.typography.bodyMedium
53 | )
54 |
55 | Spacer(modifier = Modifier.Companion.height(16.dp))
56 |
57 | Row(
58 | modifier = Modifier.Companion.fillMaxWidth(),
59 | horizontalArrangement = Arrangement.End
60 | ) {
61 | TextButton(onClick = onDismiss) {
62 | Text(stringResource(id = R.string.cancel_button))
63 | }
64 |
65 | Spacer(modifier = Modifier.Companion.width(8.dp))
66 |
67 | Button(
68 | onClick = {
69 | val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
70 | setData(Uri.fromParts("package", context.packageName, null))
71 | }
72 | context.startActivity(intent)
73 | onDismiss()
74 | }
75 | ) {
76 | Text(
77 | text = stringResource(id = R.string.location_dialog_change_settings)
78 | )
79 | }
80 | }
81 | }
82 | }
83 | }
84 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/settings/PoisonPillDialog.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.settings
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.Info
6 | import androidx.compose.material3.*
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.res.stringResource
11 | import androidx.compose.ui.unit.dp
12 | import androidx.compose.ui.window.Dialog
13 | import com.darkrockstudios.app.securecamera.R
14 |
15 | @Composable
16 | fun PoisonPillDialog(
17 | onDismiss: () -> Unit,
18 | onContinue: () -> Unit
19 | ) {
20 | Dialog(onDismissRequest = onDismiss) {
21 | Card(
22 | modifier = Modifier
23 | .fillMaxWidth()
24 | .padding(16.dp),
25 | shape = MaterialTheme.shapes.medium
26 | ) {
27 | Column(
28 | modifier = Modifier
29 | .fillMaxWidth()
30 | .padding(16.dp)
31 | ) {
32 | Row(
33 | verticalAlignment = Alignment.CenterVertically,
34 | modifier = Modifier.fillMaxWidth()
35 | ) {
36 | Icon(
37 | imageVector = Icons.Filled.Info,
38 | contentDescription = null,
39 | modifier = Modifier.size(24.dp)
40 | )
41 | Spacer(modifier = Modifier.width(8.dp))
42 | Text(
43 | text = stringResource(id = R.string.poison_pill_dialog_title),
44 | style = MaterialTheme.typography.headlineSmall
45 | )
46 | }
47 |
48 | Spacer(modifier = Modifier.height(8.dp))
49 |
50 | Text(
51 | text = stringResource(id = R.string.poison_pill_dialog_message),
52 | style = MaterialTheme.typography.bodyMedium
53 | )
54 |
55 | Spacer(modifier = Modifier.height(16.dp))
56 |
57 | Row(
58 | modifier = Modifier.fillMaxWidth(),
59 | horizontalArrangement = Arrangement.End
60 | ) {
61 | TextButton(onClick = onDismiss) {
62 | Text(stringResource(id = R.string.poison_pill_cancel_button))
63 | }
64 |
65 | Spacer(modifier = Modifier.width(8.dp))
66 |
67 | Button(
68 | onClick = onContinue,
69 | ) {
70 | Text(
71 | text = stringResource(id = R.string.poison_pill_continue_button),
72 | )
73 | }
74 | }
75 | }
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/settings/RemovePoisonPillDialog.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.settings
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.Warning
6 | import androidx.compose.material3.*
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.saveable.rememberSaveable
11 | import androidx.compose.runtime.setValue
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.unit.dp
16 | import androidx.compose.ui.window.Dialog
17 | import com.darkrockstudios.app.securecamera.R
18 |
19 | @Composable
20 | fun RemovePoisonPillDialog(
21 | onDismiss: () -> Unit,
22 | onConfirm: () -> Unit
23 | ) {
24 | var understandChecked by rememberSaveable { mutableStateOf(false) }
25 |
26 | Dialog(onDismissRequest = onDismiss) {
27 | Card(
28 | modifier = Modifier
29 | .fillMaxWidth()
30 | .padding(16.dp),
31 | shape = MaterialTheme.shapes.medium
32 | ) {
33 | Column(
34 | modifier = Modifier
35 | .fillMaxWidth()
36 | .padding(16.dp)
37 | ) {
38 | Row(
39 | verticalAlignment = Alignment.CenterVertically,
40 | modifier = Modifier.fillMaxWidth()
41 | ) {
42 | Icon(
43 | imageVector = Icons.Filled.Warning,
44 | contentDescription = null,
45 | tint = MaterialTheme.colorScheme.error,
46 | modifier = Modifier.size(24.dp)
47 | )
48 | Spacer(modifier = Modifier.width(8.dp))
49 | Text(
50 | text = stringResource(id = R.string.remove_poison_pill_dialog_title),
51 | style = MaterialTheme.typography.headlineSmall
52 | )
53 | }
54 |
55 | Spacer(modifier = Modifier.height(8.dp))
56 |
57 | Text(
58 | text = stringResource(id = R.string.remove_poison_pill_dialog_message),
59 | style = MaterialTheme.typography.bodyMedium
60 | )
61 |
62 | Spacer(modifier = Modifier.height(16.dp))
63 |
64 | // "I Understand" switch
65 | Row(
66 | modifier = Modifier.fillMaxWidth(),
67 | verticalAlignment = Alignment.CenterVertically
68 | ) {
69 | Text(
70 | text = stringResource(id = R.string.security_reset_understand),
71 | style = MaterialTheme.typography.bodyLarge,
72 | modifier = Modifier.weight(1f)
73 | )
74 | Switch(
75 | checked = understandChecked,
76 | onCheckedChange = { understandChecked = it }
77 | )
78 | }
79 |
80 | Spacer(modifier = Modifier.height(16.dp))
81 |
82 | Row(
83 | modifier = Modifier.fillMaxWidth(),
84 | horizontalArrangement = Arrangement.End
85 | ) {
86 | TextButton(onClick = onDismiss) {
87 | Text(stringResource(id = R.string.cancel_button))
88 | }
89 |
90 | Spacer(modifier = Modifier.width(8.dp))
91 |
92 | Button(
93 | onClick = onConfirm,
94 | enabled = understandChecked,
95 | colors = ButtonDefaults.buttonColors(
96 | containerColor = MaterialTheme.colorScheme.error,
97 | contentColor = MaterialTheme.colorScheme.onError
98 | )
99 | ) {
100 | Text(
101 | text = stringResource(id = R.string.remove_poison_pill_confirm_button),
102 | )
103 | }
104 | }
105 | }
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/settings/SecurityResetDialog.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.settings
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.Warning
6 | import androidx.compose.material3.*
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.saveable.rememberSaveable
11 | import androidx.compose.runtime.setValue
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.unit.dp
16 | import androidx.compose.ui.window.Dialog
17 | import com.darkrockstudios.app.securecamera.R
18 |
19 | @Composable
20 | fun SecurityResetDialog(
21 | onDismiss: () -> Unit,
22 | onConfirm: () -> Unit
23 | ) {
24 | var understandChecked by rememberSaveable { mutableStateOf(false) }
25 |
26 | Dialog(onDismissRequest = onDismiss) {
27 | Card(
28 | modifier = Modifier.Companion
29 | .fillMaxWidth()
30 | .padding(16.dp),
31 | shape = MaterialTheme.shapes.medium
32 | ) {
33 | Column(
34 | modifier = Modifier.Companion
35 | .fillMaxWidth()
36 | .padding(16.dp)
37 | ) {
38 | Row(
39 | verticalAlignment = Alignment.CenterVertically,
40 | modifier = Modifier.fillMaxWidth()
41 | ) {
42 | Icon(
43 | imageVector = Icons.Filled.Warning,
44 | contentDescription = null,
45 | tint = MaterialTheme.colorScheme.error,
46 | modifier = Modifier.size(24.dp)
47 | )
48 | Spacer(modifier = Modifier.width(8.dp))
49 | Text(
50 | text = stringResource(id = R.string.security_reset_dialog_title),
51 | style = MaterialTheme.typography.headlineSmall
52 | )
53 | }
54 |
55 | Spacer(modifier = Modifier.Companion.height(8.dp))
56 |
57 | Text(
58 | text = stringResource(id = R.string.security_reset_dialog_message),
59 | style = MaterialTheme.typography.bodyMedium
60 | )
61 |
62 | Spacer(modifier = Modifier.Companion.height(16.dp))
63 |
64 | // "I Understand" switch
65 | Row(
66 | modifier = Modifier.Companion.fillMaxWidth(),
67 | verticalAlignment = Alignment.Companion.CenterVertically
68 | ) {
69 | Text(
70 | text = stringResource(id = R.string.security_reset_understand),
71 | style = MaterialTheme.typography.bodyLarge,
72 | modifier = Modifier.Companion.weight(1f)
73 | )
74 | Switch(
75 | checked = understandChecked,
76 | onCheckedChange = { understandChecked = it }
77 | )
78 | }
79 |
80 | Spacer(modifier = Modifier.Companion.height(16.dp))
81 |
82 | Row(
83 | modifier = Modifier.Companion.fillMaxWidth(),
84 | horizontalArrangement = Arrangement.End
85 | ) {
86 | TextButton(onClick = onDismiss) {
87 | Text(stringResource(id = R.string.cancel_button))
88 | }
89 |
90 | Spacer(modifier = Modifier.Companion.width(8.dp))
91 |
92 | Button(
93 | onClick = onConfirm,
94 | enabled = understandChecked,
95 | colors = ButtonDefaults.buttonColors(
96 | containerColor = MaterialTheme.colorScheme.error,
97 | contentColor = MaterialTheme.colorScheme.onError
98 | )
99 | ) {
100 | Text(
101 | text = stringResource(id = R.string.security_reset_destroy_button),
102 | )
103 | }
104 | }
105 | }
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/share/ShareUtils.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.share
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import com.darkrockstudios.app.securecamera.camera.PhotoDef
7 |
8 | private const val DECRYPTING_PROVIDER_AUTHORITY = ".decryptingprovider"
9 |
10 | /**
11 | * Creates a URI for a photo using the DecryptingImageProvider
12 | */
13 | private fun getDecryptingFileProviderUri(
14 | photo: PhotoDef,
15 | context: Context
16 | ): Uri {
17 | val authority = context.packageName + DECRYPTING_PROVIDER_AUTHORITY
18 | return Uri.Builder()
19 | .scheme("content")
20 | .authority(authority)
21 | .path("photos/${photo.photoName}")
22 | .build()
23 | }
24 |
25 | /**
26 | * Share a photo using DecryptingImageProvider (no temp files)
27 | */
28 | fun sharePhotoWithProvider(
29 | photo: PhotoDef,
30 | context: Context
31 | ): Boolean {
32 | val uri = getDecryptingFileProviderUri(photo, context)
33 |
34 | val shareIntent = Intent().apply {
35 | action = Intent.ACTION_SEND
36 | putExtra(Intent.EXTRA_STREAM, uri)
37 | type = "image/jpeg"
38 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
39 | }
40 |
41 | context.startActivity(Intent.createChooser(shareIntent, "Share Photo"))
42 | return true
43 | }
44 |
45 | /**
46 | * Share multiple photos using DecryptingImageProvider (no temp files)
47 | */
48 | fun sharePhotosWithProvider(
49 | photos: List,
50 | context: Context
51 | ): Boolean {
52 | if (photos.isEmpty()) {
53 | return false
54 | }
55 |
56 | val uris = photos.map { photo ->
57 | getDecryptingFileProviderUri(photo, context)
58 | }
59 |
60 | val shareIntent = if (uris.size == 1) {
61 | // Single photo share
62 | Intent().apply {
63 | action = Intent.ACTION_SEND
64 | putExtra(Intent.EXTRA_STREAM, uris.first())
65 | type = "image/jpeg"
66 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
67 | }
68 | } else {
69 | // Multiple photo share
70 | Intent().apply {
71 | action = Intent.ACTION_SEND_MULTIPLE
72 | putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
73 | type = "image/jpeg"
74 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
75 | }
76 | }
77 |
78 | context.startActivity(Intent.createChooser(shareIntent, "Share Photos"))
79 | return true
80 | }
81 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/ui/NotificationPermissionRationale.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.ui
2 |
3 | import android.Manifest
4 | import android.content.pm.PackageManager
5 | import android.os.Build
6 | import androidx.activity.compose.rememberLauncherForActivityResult
7 | import androidx.activity.result.contract.ActivityResultContracts
8 | import androidx.annotation.StringRes
9 | import androidx.compose.material3.AlertDialog
10 | import androidx.compose.material3.Text
11 | import androidx.compose.material3.TextButton
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.LaunchedEffect
14 | import androidx.compose.runtime.mutableStateOf
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.ui.platform.LocalContext
17 | import androidx.compose.ui.res.stringResource
18 | import androidx.core.content.ContextCompat
19 | import com.darkrockstudios.app.securecamera.R
20 |
21 | @Composable
22 | fun NotificationPermissionRationale(
23 | @StringRes title: Int,
24 | @StringRes text: Int,
25 | ) {
26 | val context = LocalContext.current
27 |
28 | val showNotificationPermissionDialog = remember { mutableStateOf(false) }
29 |
30 | val notificationPermissionLauncher = rememberLauncherForActivityResult(
31 | contract = ActivityResultContracts.RequestPermission()
32 | ) { _ ->
33 | // Noop
34 | }
35 |
36 | // Check if we need to request notification permission (API 33+)
37 | LaunchedEffect(Unit) {
38 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
39 | val notificationPermission = Manifest.permission.POST_NOTIFICATIONS
40 | if (ContextCompat.checkSelfPermission(
41 | context,
42 | notificationPermission
43 | ) != PackageManager.PERMISSION_GRANTED
44 | ) {
45 | showNotificationPermissionDialog.value = true
46 | }
47 | }
48 | }
49 |
50 | // Notification permission dialog (API 33+)
51 | if (showNotificationPermissionDialog.value) {
52 | AlertDialog(
53 | onDismissRequest = { showNotificationPermissionDialog.value = false },
54 | title = { Text(stringResource(id = title)) },
55 | text = { Text(stringResource(id = text)) },
56 | confirmButton = {
57 | TextButton(
58 | onClick = {
59 | showNotificationPermissionDialog.value = false
60 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
61 | notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
62 | }
63 | }
64 | ) {
65 | Text(stringResource(id = R.string.notification_permission_button))
66 | }
67 | },
68 | dismissButton = {
69 | TextButton(
70 | onClick = { showNotificationPermissionDialog.value = false }
71 | ) {
72 | Text(stringResource(id = R.string.cancel_button))
73 | }
74 | }
75 | )
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/ui/PlayfulScaleVisibility.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.ui
2 |
3 | import androidx.compose.animation.*
4 | import androidx.compose.animation.core.FastOutSlowInEasing
5 | import androidx.compose.animation.core.Spring
6 | import androidx.compose.animation.core.spring
7 | import androidx.compose.animation.core.tween
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 |
11 | @Composable
12 | fun PlayfulScaleVisibility(
13 | isVisible: Boolean,
14 | modifier: Modifier = Modifier,
15 | content: @Composable () -> Unit
16 | ) {
17 | AnimatedVisibility(
18 | modifier = modifier,
19 | visible = isVisible,
20 | enter = scaleIn(
21 | initialScale = 0.7f,
22 | animationSpec = spring(
23 | stiffness = Spring.StiffnessLow,
24 | dampingRatio = Spring.DampingRatioMediumBouncy
25 | )
26 | ) + fadeIn(),
27 | exit = scaleOut(
28 | targetScale = 0.7f,
29 | animationSpec = tween(
30 | durationMillis = 250,
31 | easing = FastOutSlowInEasing
32 | )
33 | ) + fadeOut()
34 | ) {
35 | content()
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/ui/UiEvent.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.ui
2 |
3 | sealed interface UiEvent {
4 | data class ShowSnack(
5 | val text: String,
6 | val action: String? = null
7 | ) : UiEvent
8 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/ui/UiEventHandler.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.ui
2 |
3 | import androidx.compose.material3.SnackbarHostState
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.navigation.NavController
7 | import kotlinx.coroutines.flow.SharedFlow
8 |
9 | @Composable
10 | fun HandleUiEvents(events: SharedFlow, snackbarHostState: SnackbarHostState, navController: NavController) {
11 | LaunchedEffect(Unit) {
12 | events.collect { e ->
13 | when (e) {
14 | is UiEvent.ShowSnack -> {
15 | val result = snackbarHostState.showSnackbar(
16 | message = e.text,
17 | actionLabel = e.action
18 | )
19 | // handle SnackbarResult if you need to react to “Undo”, etc.
20 | }
21 | }
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.*
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.platform.LocalContext
8 |
9 | private val DarkColorScheme = darkColorScheme(
10 | primary = Purple80,
11 | secondary = PurpleGrey80,
12 | tertiary = Pink80
13 | )
14 |
15 | private val LightColorScheme = lightColorScheme(
16 | primary = Purple40,
17 | secondary = PurpleGrey40,
18 | tertiary = Pink40
19 |
20 | /* Other default colors to override
21 | background = Color(0xFFFFFBFE),
22 | surface = Color(0xFFFFFBFE),
23 | onPrimary = Color.White,
24 | onSecondary = Color.White,
25 | onTertiary = Color.White,
26 | onBackground = Color(0xFF1C1B1F),
27 | onSurface = Color(0xFF1C1B1F),
28 | */
29 | )
30 |
31 | @Composable
32 | fun SecureCameraTheme(
33 | darkTheme: Boolean = isSystemInDarkTheme(),
34 | // Dynamic color is available on Android 12+
35 | dynamicColor: Boolean = true,
36 | content: @Composable () -> Unit
37 | ) {
38 | val colorScheme = when {
39 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
40 | val context = LocalContext.current
41 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
42 | }
43 |
44 | darkTheme -> DarkColorScheme
45 | else -> LightColorScheme
46 | }
47 |
48 | MaterialTheme(
49 | colorScheme = colorScheme,
50 | typography = Typography,
51 | content = content
52 | )
53 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/usecases/CreatePinUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.usecases
2 |
3 | import com.darkrockstudios.app.securecamera.auth.AuthorizationRepository
4 | import com.darkrockstudios.app.securecamera.preferences.AppPreferencesDataSource
5 | import com.darkrockstudios.app.securecamera.security.SchemeConfig
6 | import com.darkrockstudios.app.securecamera.security.pin.PinRepository
7 | import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
8 |
9 | class CreatePinUseCase(
10 | private val authorizationRepository: AuthorizationRepository,
11 | private val encryptionScheme: EncryptionScheme,
12 | private val pinRepository: PinRepository,
13 | private val preferencesDataSource: AppPreferencesDataSource,
14 | ) {
15 | suspend fun createPin(pin: String, schemeConfig: SchemeConfig): Boolean {
16 | pinRepository.setAppPin(pin, schemeConfig)
17 | val hashedPin = authorizationRepository.verifyPin(pin)
18 | return if (hashedPin != null) {
19 | authorizationRepository.createKey(pin, hashedPin)
20 | encryptionScheme.deriveAndCacheKey(pin, hashedPin)
21 | preferencesDataSource.setIntroCompleted(true)
22 | true
23 | } else {
24 | false
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/usecases/InvalidateSessionUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.usecases
2 |
3 | import com.darkrockstudios.app.securecamera.auth.AuthorizationRepository
4 | import com.darkrockstudios.app.securecamera.camera.SecureImageRepository
5 |
6 | class InvalidateSessionUseCase(
7 | private val imageManager: SecureImageRepository,
8 | private val authManager: AuthorizationRepository,
9 | ) {
10 | fun invalidateSession() {
11 | imageManager.evictKey()
12 | imageManager.thumbnailCache.clear()
13 | authManager.revokeAuthorization()
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/usecases/PinSizeUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.usecases
2 |
3 | import com.darkrockstudios.app.securecamera.security.SecurityLevel
4 | import com.darkrockstudios.app.securecamera.security.SecurityLevelDetector
5 |
6 | class PinSizeUseCase(
7 | val securityLevelDetector: SecurityLevelDetector
8 | ) {
9 | fun getPinSizeRange(level: SecurityLevel = securityLevelDetector.detectSecurityLevel()): IntRange {
10 | return when (level) {
11 | SecurityLevel.TEE, SecurityLevel.STRONGBOX -> (4..16)
12 | SecurityLevel.SOFTWARE -> (6..16)
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/usecases/PinStrengthCheckUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.usecases
2 |
3 | class PinStrengthCheckUseCase {
4 | fun isPinStrongEnough(pin: String): Boolean {
5 | // Check if PIN is at least 4 digits long and contains only digits
6 | if (pin.length < 4 || !pin.all { it.isDigit() }) {
7 | return false
8 | }
9 |
10 | // Check if all digits are the same (e.g., "1111")
11 | if (pin.all { it == pin[0] }) {
12 | return false
13 | }
14 |
15 | // Check if PIN is a sequence (ascending or descending)
16 | val isAscendingSequence = (0 until pin.length - 1).all {
17 | pin[it + 1].digitToInt() - pin[it].digitToInt() == 1
18 | }
19 |
20 | val isDescendingSequence = (0 until pin.length - 1).all {
21 | pin[it + 1].digitToInt() - pin[it].digitToInt() == -1
22 | }
23 |
24 | if (isAscendingSequence || isDescendingSequence) {
25 | return false
26 | }
27 |
28 | if (blackList.contains(pin)) {
29 | return false
30 | }
31 |
32 | return true
33 | }
34 |
35 | companion object {
36 | /**
37 | * These are some of the most frequently chosen PINs in data leaks
38 | * that are not already covered by our other heuristics.
39 | */
40 | val blackList = listOf(
41 | "1212",
42 | "6969",
43 | )
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/usecases/RemovePoisonPillIUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.usecases
2 |
3 | import com.darkrockstudios.app.securecamera.camera.SecureImageRepository
4 | import com.darkrockstudios.app.securecamera.security.pin.PinRepository
5 |
6 | class RemovePoisonPillIUseCase(
7 | private val pinRepository: PinRepository,
8 | private val imageManager: SecureImageRepository,
9 | ) {
10 | suspend fun removePoisonPill() {
11 | pinRepository.removePoisonPillPin()
12 | imageManager.removeAllDecoyPhotos()
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/usecases/SecurityResetUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.usecases
2 |
3 | import com.darkrockstudios.app.securecamera.auth.AuthorizationRepository
4 | import com.darkrockstudios.app.securecamera.camera.SecureImageRepository
5 | import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
6 | import timber.log.Timber
7 |
8 | class SecurityResetUseCase(
9 | private val authManager: AuthorizationRepository,
10 | private val imageManager: SecureImageRepository,
11 | private val encryptionScheme: EncryptionScheme,
12 | ) {
13 | suspend fun reset() {
14 | authManager.securityFailureReset()
15 | imageManager.securityFailureReset()
16 | encryptionScheme.securityFailureReset()
17 | Timber.d("Security Reset Complete!")
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/usecases/VerifyPinUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.usecases
2 |
3 | import com.darkrockstudios.app.securecamera.auth.AuthorizationRepository
4 | import com.darkrockstudios.app.securecamera.camera.SecureImageRepository
5 | import com.darkrockstudios.app.securecamera.security.pin.PinRepository
6 | import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
7 |
8 | class VerifyPinUseCase(
9 | private val authManager: AuthorizationRepository,
10 | private val imageManager: SecureImageRepository,
11 | private val pinRepository: PinRepository,
12 | private val encryptionScheme: EncryptionScheme,
13 | private val migratePinHash: MigratePinHash,
14 | ) {
15 | suspend fun verifyPin(pin: String): Boolean {
16 | migratePinHash.runMigration(pin)
17 |
18 | if (pinRepository.hasPoisonPillPin() && pinRepository.verifyPoisonPillPin(pin)) {
19 | encryptionScheme.activatePoisonPill(oldPin = pinRepository.getHashedPin())
20 | imageManager.activatePoisonPill()
21 | authManager.activatePoisonPill()
22 | }
23 |
24 | val hashedPin = authManager.verifyPin(pin)
25 | return if (hashedPin != null) {
26 | encryptionScheme.deriveAndCacheKey(pin, hashedPin)
27 | authManager.resetFailedAttempts()
28 | true
29 | } else {
30 | false
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/darkrockstudios/app/securecamera/viewphoto/PhotoInfoDialog.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.viewphoto
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.height
6 | import androidx.compose.foundation.rememberScrollState
7 | import androidx.compose.foundation.verticalScroll
8 | import androidx.compose.material3.AlertDialog
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Text
11 | import androidx.compose.material3.TextButton
12 | import androidx.compose.runtime.*
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.unit.dp
16 | import com.darkrockstudios.app.securecamera.R
17 | import com.darkrockstudios.app.securecamera.camera.PhotoDef
18 | import com.darkrockstudios.app.securecamera.camera.PhotoMetaData
19 | import com.darkrockstudios.app.securecamera.camera.SecureImageRepository
20 | import org.koin.compose.koinInject
21 |
22 | @Composable
23 | fun PhotoInfoDialog(
24 | photo: PhotoDef,
25 | dismiss: () -> Unit
26 | ) {
27 | val imageManager = koinInject()
28 |
29 | var metadata by remember { mutableStateOf(null) }
30 |
31 | LaunchedEffect(photo) {
32 | metadata = imageManager.getPhotoMetaData(photo)
33 | }
34 |
35 | AlertDialog(
36 | onDismissRequest = dismiss,
37 | title = { Text(stringResource(id = R.string.info_dialog_title)) },
38 | text = {
39 | Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
40 |
41 | InfoRow(
42 | title = stringResource(id = R.string.photo_name_label),
43 | value = photo.photoName,
44 | )
45 |
46 | InfoRow(
47 | title = stringResource(id = R.string.photo_resolution_label),
48 | value = metadata?.resolutionString() ?: stringResource(R.string.photo_no_data),
49 | )
50 |
51 | InfoRow(
52 | title = stringResource(id = R.string.photo_date_label),
53 | value = metadata?.dateTaken?.toString() ?: stringResource(R.string.photo_no_data),
54 | )
55 |
56 | InfoRow(
57 | title = stringResource(id = R.string.photo_location_label),
58 | value = metadata?.location?.latLongString ?: stringResource(R.string.photo_no_data),
59 | )
60 |
61 | InfoRow(
62 | title = stringResource(id = R.string.photo_orientation_label),
63 | value = metadata?.orientation?.toString() ?: stringResource(R.string.photo_no_data),
64 | )
65 | }
66 | },
67 | confirmButton = {
68 | TextButton(onClick = dismiss) {
69 | Text(stringResource(id = R.string.ok_button))
70 | }
71 | }
72 | )
73 | }
74 |
75 | @Composable
76 | private fun InfoRow(
77 | title: String,
78 | value: String
79 | ) {
80 | Text(
81 | text = title,
82 | style = MaterialTheme.typography.bodyLarge,
83 | color = MaterialTheme.colorScheme.onSurface
84 | )
85 | Spacer(modifier = Modifier.Companion.height(4.dp))
86 |
87 | Text(
88 | text = value,
89 | style = MaterialTheme.typography.bodyLarge,
90 | color = MaterialTheme.colorScheme.onSurface
91 | )
92 |
93 | Spacer(modifier = Modifier.Companion.height(16.dp))
94 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_monochrome.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3DDC84
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/file_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/oss/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/oss/kotlin/com/darkrockstudios/app/securecamera/OssSnapSafeApplication.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import com.darkrockstudios.app.securecamera.obfuscation.AndroidFacialDetection
4 | import com.darkrockstudios.app.securecamera.obfuscation.FacialDetection
5 | import org.koin.core.module.Module
6 | import org.koin.core.module.dsl.factoryOf
7 | import org.koin.dsl.bind
8 | import org.koin.dsl.module
9 |
10 | class OssSnapSafeApplication : SnapSafeApplication() {
11 | override fun flavorModule(): Module = module {
12 | factoryOf(::AndroidFacialDetection) bind FacialDetection::class
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/oss/kotlin/com/darkrockstudios/app/securecamera/obfuscation/AndroidFacialDetection.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.obfuscation
2 |
3 | import android.graphics.Bitmap
4 | import android.graphics.Canvas
5 | import android.graphics.PointF
6 | import android.graphics.Rect
7 | import android.media.FaceDetector
8 | import androidx.core.graphics.createBitmap
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.withContext
11 | import timber.log.Timber
12 |
13 | class AndroidFacialDetection : FacialDetection {
14 | override suspend fun processForFaces(bitmap: Bitmap): List =
15 | withContext(Dispatchers.Default) {
16 | // Android's FaceDetector requires RGB_565 format
17 | val bitmapForDetection = if (bitmap.config != Bitmap.Config.RGB_565) {
18 | try {
19 | createBitmap(bitmap.width, bitmap.height, Bitmap.Config.RGB_565).also { targetBitmap ->
20 | val canvas = Canvas(targetBitmap)
21 | canvas.drawBitmap(bitmap, 0f, 0f, null)
22 | }
23 | } catch (e: Exception) {
24 | Timber.Forest.e(e, "Failed to convert bitmap to RGB_565")
25 | return@withContext emptyList()
26 | }
27 | } else {
28 | bitmap
29 | }
30 |
31 | // Maximum number of faces to detect
32 | val maxFaces = 100
33 |
34 | try {
35 | val detector = FaceDetector(bitmapForDetection.width, bitmapForDetection.height, maxFaces)
36 | val faces = arrayOfNulls(maxFaces)
37 |
38 | // Find faces in the bitmap
39 | val facesFound = detector.findFaces(bitmapForDetection, faces)
40 | Timber.Forest.d("Found $facesFound faces using Android FaceDetector")
41 |
42 | // Convert the detected faces to our FoundFace format
43 | val foundFaces = mutableListOf()
44 |
45 | for (i in 0 until facesFound) {
46 | faces[i]?.let { face ->
47 | val midPoint = PointF()
48 | face.getMidPoint(midPoint)
49 |
50 | // Calculate the bounding box based on the face's position and confidence
51 | val eyeDistance = face.eyesDistance()
52 | val confidence = face.confidence()
53 |
54 | // Use eye distance to estimate face size
55 | // The multipliers are approximations and may need adjustment
56 | val faceWidth = (eyeDistance * 2.5f).toInt()
57 | val faceHeight = (eyeDistance * 3.0f).toInt()
58 |
59 | val left = (midPoint.x - faceWidth / 2).toInt().coerceAtLeast(0)
60 | val top = (midPoint.y - faceHeight / 2).toInt().coerceAtLeast(0)
61 | val right = (midPoint.x + faceWidth / 2).toInt().coerceAtMost(bitmap.width)
62 | val bottom = (midPoint.y + faceHeight / 2).toInt().coerceAtMost(bitmap.height)
63 |
64 | val boundingBox = Rect(left, top, right, bottom)
65 |
66 | // Calculate approximate eye positions based on the midpoint and eye distance
67 | val leftEyeX = midPoint.x - eyeDistance / 2
68 | val rightEyeX = midPoint.x + eyeDistance / 2
69 | val eyeY = midPoint.y - eyeDistance / 8 // Slight adjustment for eye height
70 |
71 | val eyes = FacialDetection.FoundFace.Eyes(
72 | left = PointF(leftEyeX, eyeY),
73 | right = PointF(rightEyeX, eyeY)
74 | )
75 |
76 | foundFaces.add(
77 | FacialDetection.FoundFace(
78 | boundingBox = boundingBox,
79 | eyes = eyes
80 | )
81 | )
82 | }
83 | }
84 |
85 | // Clean up if we created a new bitmap
86 | if (bitmapForDetection != bitmap) {
87 | bitmapForDetection.recycle()
88 | }
89 |
90 | foundFaces
91 | } catch (e: Exception) {
92 | Timber.Forest.e(e, "Error detecting faces with Android FaceDetector")
93 | emptyList()
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/app/src/test/kotlin/com/darkrockstudios/app/securecamera/TestClock.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera
2 |
3 | import kotlin.time.Clock
4 | import kotlin.time.Instant
5 |
6 | class TestClock(var fixedInstant: Instant) : Clock {
7 | override fun now(): Instant = fixedInstant
8 | }
--------------------------------------------------------------------------------
/app/src/test/kotlin/com/darkrockstudios/app/securecamera/camera/PhotoDefTest.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.camera
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 | import java.io.File
6 | import java.text.SimpleDateFormat
7 | import java.util.*
8 |
9 | class PhotoDefTest {
10 |
11 | @Test
12 | fun `dateTaken parses valid photoName correctly`() {
13 | // Create a known date
14 | val calendar = Calendar.getInstance()
15 | calendar.set(2023, Calendar.JANUARY, 15, 10, 30, 45)
16 | calendar.set(Calendar.MILLISECOND, 500)
17 | val expectedDate = calendar.time
18 |
19 | // Format the date as it would be in a photoName
20 | val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss_SS", Locale.US)
21 | val dateString = dateFormat.format(expectedDate)
22 | val photoName = "photo_$dateString.jpg"
23 |
24 | // Create a PhotoDef with this photoName
25 | val photoDef = PhotoDef(
26 | photoName = photoName,
27 | photoFormat = "jpg",
28 | photoFile = File("dummy/path")
29 | )
30 |
31 | // Get the parsed date
32 | val parsedDate = photoDef.dateTaken()
33 |
34 | // Compare the dates (using string representation to avoid millisecond precision issues)
35 | assertEquals(dateFormat.format(expectedDate), dateFormat.format(parsedDate))
36 | }
37 |
38 | @Test
39 | fun `dateTaken handles invalid photoName gracefully`() {
40 | // Create a PhotoDef with an invalid photoName
41 | val photoDef = PhotoDef(
42 | photoName = "invalid_photo_name.jpg",
43 | photoFormat = "jpg",
44 | photoFile = File("dummy/path")
45 | )
46 |
47 | // Get the current time before calling dateTaken
48 | val beforeTime = Date()
49 |
50 | // Call dateTaken which should return current date for invalid format
51 | val parsedDate = photoDef.dateTaken()
52 |
53 | // Get the current time after calling dateTaken
54 | val afterTime = Date()
55 |
56 | // Verify the returned date is between beforeTime and afterTime
57 | // This is a loose check since we can't predict the exact time it will use
58 | assert(parsedDate.time >= beforeTime.time - 1000 && parsedDate.time <= afterTime.time + 1000)
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/test/kotlin/com/darkrockstudios/app/securecamera/navigation/AppDestinationsTest.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.navigation
2 |
3 | import androidx.navigation.NavDestination
4 | import com.darkrockstudios.app.securecamera.navigation.AppDestinations.decodeReturnRoute
5 | import com.darkrockstudios.app.securecamera.navigation.AppDestinations.encodeReturnRoute
6 | import com.darkrockstudios.app.securecamera.navigation.AppDestinations.isPinVerificationRoute
7 | import io.mockk.every
8 | import io.mockk.mockk
9 | import org.junit.Test
10 | import kotlin.test.assertEquals
11 | import kotlin.test.assertFalse
12 | import kotlin.test.assertTrue
13 |
14 | class AppDestinationsTest {
15 |
16 | @Test
17 | fun `encodeReturnRoute should encode simple string`() {
18 | // Given
19 | val route = "camera"
20 |
21 | // When
22 | val encoded = encodeReturnRoute(route)
23 |
24 | // Then
25 | assertEquals("Y2FtZXJh", encoded)
26 | }
27 |
28 | @Test
29 | fun `encodeReturnRoute should encode empty string`() {
30 | // Given
31 | val route = ""
32 |
33 | // When
34 | val encoded = encodeReturnRoute(route)
35 |
36 | // Then
37 | assertEquals("", encoded)
38 | }
39 |
40 | @Test
41 | fun `encodeReturnRoute should encode string with special characters`() {
42 | // Given
43 | val route = "view_photo/image?with=special&chars"
44 |
45 | // When
46 | val encoded = encodeReturnRoute(route)
47 |
48 | // Then
49 | assertEquals("dmlld19waG90by9pbWFnZT93aXRoPXNwZWNpYWwmY2hhcnM=", encoded)
50 | }
51 |
52 | @Test
53 | fun `decodeReturnRoute should decode encoded simple string`() {
54 | // Given
55 | val encoded = "Y2FtZXJh"
56 |
57 | // When
58 | val decoded = decodeReturnRoute(encoded)
59 |
60 | // Then
61 | assertEquals("camera", decoded)
62 | }
63 |
64 | @Test
65 | fun `decodeReturnRoute should decode encoded empty string`() {
66 | // Given
67 | val encoded = ""
68 |
69 | // When
70 | val decoded = decodeReturnRoute(encoded)
71 |
72 | // Then
73 | assertEquals("", decoded)
74 | }
75 |
76 | @Test
77 | fun `decodeReturnRoute should decode encoded string with special characters`() {
78 | // Given
79 | val encoded = "dmlld19waG90by9pbWFnZT93aXRoPXNwZWNpYWwmY2hhcnM="
80 |
81 | // When
82 | val decoded = decodeReturnRoute(encoded)
83 |
84 | // Then
85 | assertEquals("view_photo/image?with=special&chars", decoded)
86 | }
87 |
88 | @Test
89 | fun `encode and decode should be reversible`() {
90 | // Given
91 | val originalRoutes = listOf(
92 | "camera",
93 | "",
94 | "view_photo/image?with=special&chars",
95 | "settings/advanced#section",
96 | "gallery/folder/subfolder"
97 | )
98 |
99 | // When & Then
100 | originalRoutes.forEach { route ->
101 | val encoded = encodeReturnRoute(route)
102 | val decoded = decodeReturnRoute(encoded)
103 | assertEquals(route, decoded, "Failed for route: $route")
104 | }
105 | }
106 |
107 | @Test
108 | fun `isPinVerificationRoute should return true for pin verification route`() {
109 | // Given
110 | val navDestination = mockk()
111 | every { navDestination.route } returns "pin_verification/Y2FtZXJh"
112 |
113 | // When
114 | val result = isPinVerificationRoute(navDestination)
115 |
116 | // Then
117 | assertTrue(result)
118 | }
119 |
120 | @Test
121 | fun `isPinVerificationRoute should return false for non-pin verification route`() {
122 | // Given
123 | val navDestination = mockk()
124 | every { navDestination.route } returns "camera"
125 |
126 | // When
127 | val result = isPinVerificationRoute(navDestination)
128 |
129 | // Then
130 | assertFalse(result)
131 | }
132 |
133 | @Test
134 | fun `isPinVerificationRoute should return false for null route`() {
135 | // Given
136 | val navDestination = mockk()
137 | every { navDestination.route } returns null
138 |
139 | // When
140 | val result = isPinVerificationRoute(navDestination)
141 |
142 | // Then
143 | assertFalse(result)
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/app/src/test/kotlin/com/darkrockstudios/app/securecamera/security/ShardedKeyTest.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.security
2 |
3 | import org.junit.Test
4 | import kotlin.test.assertContentEquals
5 | import kotlin.test.assertNotEquals
6 |
7 | class ShardedKeyTest {
8 |
9 | @Test
10 | fun `constructor should split key into two parts`() {
11 | // Given
12 | val originalKey = ByteArray(32) { it.toByte() }
13 |
14 | // When
15 | val shardedKey = ShardedKey(originalKey)
16 |
17 | // Then
18 | // We can't directly test private fields, but we can verify the key is reconstructed correctly
19 | val reconstructed = shardedKey.reconstructKey()
20 | assertContentEquals(originalKey, reconstructed, "Reconstructed key should match original")
21 | }
22 |
23 | @Test
24 | fun `reconstructKey should return original key`() {
25 | // Given
26 | val originalKey = ByteArray(16) { (it * 2).toByte() }
27 | val shardedKey = ShardedKey(originalKey)
28 |
29 | // When
30 | val reconstructed = shardedKey.reconstructKey()
31 |
32 | // Then
33 | assertContentEquals(originalKey, reconstructed, "Reconstructed key should match original")
34 | }
35 |
36 | @Test
37 | fun `evict should clear key parts`() {
38 | // Given
39 | val originalKey = ByteArray(24) { it.toByte() }
40 | val shardedKey = ShardedKey(originalKey)
41 |
42 | // When
43 | val beforeEviction = shardedKey.reconstructKey()
44 | shardedKey.evict()
45 | val afterEviction = shardedKey.reconstructKey()
46 |
47 | // Then
48 | assertContentEquals(originalKey, beforeEviction, "Key should be correct before eviction")
49 |
50 | // After eviction, the reconstructed key should be all zeros
51 | val allZeros = ByteArray(originalKey.size)
52 | assertContentEquals(allZeros, afterEviction, "After eviction, reconstructed key should be all zeros")
53 | }
54 |
55 | @Test
56 | fun `should work with empty key`() {
57 | // Given
58 | val emptyKey = ByteArray(0)
59 |
60 | // When
61 | val shardedKey = ShardedKey(emptyKey)
62 | val reconstructed = shardedKey.reconstructKey()
63 |
64 | // Then
65 | assertContentEquals(emptyKey, reconstructed, "Should handle empty keys correctly")
66 | }
67 |
68 | @Test
69 | fun `should work with large keys`() {
70 | // Given
71 | val largeKey = ByteArray(1024) { (it % 256).toByte() }
72 |
73 | // When
74 | val shardedKey = ShardedKey(largeKey)
75 | val reconstructed = shardedKey.reconstructKey()
76 |
77 | // Then
78 | assertContentEquals(largeKey, reconstructed, "Should handle large keys correctly")
79 | }
80 |
81 | @Test
82 | fun `multiple reconstructions should return same key`() {
83 | // Given
84 | val originalKey = ByteArray(32) { it.toByte() }
85 | val shardedKey = ShardedKey(originalKey)
86 |
87 | // When
88 | val firstReconstruction = shardedKey.reconstructKey()
89 | val secondReconstruction = shardedKey.reconstructKey()
90 |
91 | // Then
92 | assertContentEquals(
93 | firstReconstruction,
94 | secondReconstruction,
95 | "Multiple reconstructions should return the same key"
96 | )
97 | }
98 |
99 | @Test
100 | fun `different keys should produce different sharded representations`() {
101 | // Given
102 | val key1 = ByteArray(32) { it.toByte() }
103 | val key2 = ByteArray(32) { (it + 1).toByte() }
104 |
105 | // When
106 | val shardedKey1 = ShardedKey(key1)
107 | val shardedKey2 = ShardedKey(key2)
108 |
109 | // Then
110 | // We can't directly compare private fields, but we can verify the reconstructed keys are different
111 | val reconstructed1 = shardedKey1.reconstructKey()
112 | val reconstructed2 = shardedKey2.reconstructKey()
113 |
114 | assertContentEquals(key1, reconstructed1, "First key should be reconstructed correctly")
115 | assertContentEquals(key2, reconstructed2, "Second key should be reconstructed correctly")
116 | assertNotEquals(
117 | reconstructed1.toList(),
118 | reconstructed2.toList(),
119 | "Different keys should produce different reconstructed values"
120 | )
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/app/src/test/kotlin/com/darkrockstudios/app/securecamera/usecases/PinSizeUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.usecases
2 |
3 | import com.darkrockstudios.app.securecamera.security.SecurityLevel
4 | import com.darkrockstudios.app.securecamera.security.SecurityLevelDetector
5 | import io.mockk.every
6 | import io.mockk.mockk
7 | import org.junit.Assert.assertEquals
8 | import org.junit.Before
9 | import org.junit.Test
10 |
11 | class PinSizeUseCaseTest {
12 |
13 | private lateinit var securityLevelDetector: SecurityLevelDetector
14 | private lateinit var pinSizeUseCase: PinSizeUseCase
15 |
16 | @Before
17 | fun setup() {
18 | securityLevelDetector = mockk()
19 | pinSizeUseCase = PinSizeUseCase(securityLevelDetector)
20 | }
21 |
22 | @Test
23 | fun `getPinSizeRange should return 4-16 for TEE security level`() {
24 | // Given
25 | val securityLevel = SecurityLevel.TEE
26 |
27 | // When
28 | val result = pinSizeUseCase.getPinSizeRange(securityLevel)
29 |
30 | // Then
31 | assertEquals(4..16, result)
32 | }
33 |
34 | @Test
35 | fun `getPinSizeRange should return 4-16 for STRONGBOX security level`() {
36 | // Given
37 | val securityLevel = SecurityLevel.STRONGBOX
38 |
39 | // When
40 | val result = pinSizeUseCase.getPinSizeRange(securityLevel)
41 |
42 | // Then
43 | assertEquals(4..16, result)
44 | }
45 |
46 | @Test
47 | fun `getPinSizeRange should return 6-16 for SOFTWARE security level`() {
48 | // Given
49 | val securityLevel = SecurityLevel.SOFTWARE
50 |
51 | // When
52 | val result = pinSizeUseCase.getPinSizeRange(securityLevel)
53 |
54 | // Then
55 | assertEquals(6..16, result)
56 | }
57 |
58 | @Test
59 | fun `getPinSizeRange should use detected security level when not provided`() {
60 | // Given
61 | every { securityLevelDetector.detectSecurityLevel() } returns SecurityLevel.TEE
62 |
63 | // When
64 | val result = pinSizeUseCase.getPinSizeRange()
65 |
66 | // Then
67 | assertEquals(4..16, result)
68 | }
69 |
70 | @Test
71 | fun `getPinSizeRange should use detected security level when not provided - SOFTWARE case`() {
72 | // Given
73 | every { securityLevelDetector.detectSecurityLevel() } returns SecurityLevel.SOFTWARE
74 |
75 | // When
76 | val result = pinSizeUseCase.getPinSizeRange()
77 |
78 | // Then
79 | assertEquals(6..16, result)
80 | }
81 |
82 | @Test
83 | fun `getPinSizeRange should use detected security level when not provided - STRONGBOX case`() {
84 | // Given
85 | every { securityLevelDetector.detectSecurityLevel() } returns SecurityLevel.STRONGBOX
86 |
87 | // When
88 | val result = pinSizeUseCase.getPinSizeRange()
89 |
90 | // Then
91 | assertEquals(4..16, result)
92 | }
93 | }
--------------------------------------------------------------------------------
/app/src/test/kotlin/com/darkrockstudios/app/securecamera/usecases/PinStrengthCheckUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.darkrockstudios.app.securecamera.usecases
2 |
3 | import org.junit.Assert.assertFalse
4 | import org.junit.Assert.assertTrue
5 | import org.junit.Before
6 | import org.junit.Test
7 |
8 | class PinStrengthCheckUseCaseTest {
9 |
10 | private lateinit var pinStrengthCheckUseCase: PinStrengthCheckUseCase
11 |
12 | @Before
13 | fun setup() {
14 | pinStrengthCheckUseCase = PinStrengthCheckUseCase()
15 | }
16 |
17 | @Test
18 | fun `test valid PINs`() {
19 | // Valid PINs: at least 4 digits, not all the same, not sequential
20 | assertTrue(pinStrengthCheckUseCase.isPinStrongEnough("1357")) // 4 digits, not all same, not sequential
21 | assertTrue(pinStrengthCheckUseCase.isPinStrongEnough("2468")) // 4 digits, not all same, not sequential
22 | assertTrue(pinStrengthCheckUseCase.isPinStrongEnough("1593")) // 4 digits, not all same, not sequential
23 | assertTrue(pinStrengthCheckUseCase.isPinStrongEnough("7294")) // 4 digits, not all same, not sequential
24 | assertTrue(pinStrengthCheckUseCase.isPinStrongEnough("13579")) // 5 digits, not all same, not sequential
25 | assertTrue(pinStrengthCheckUseCase.isPinStrongEnough("24680")) // 5 digits, not all same, not sequential
26 | }
27 |
28 | @Test
29 | fun `test invalid PINs - too short`() {
30 | // PINs with less than 4 digits
31 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("123")) // 3 digits
32 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("12")) // 2 digits
33 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("1")) // 1 digit
34 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("")) // Empty string
35 | }
36 |
37 | @Test
38 | fun `test invalid PINs - non-numeric`() {
39 | // PINs with non-numeric characters
40 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("123a")) // Contains letter
41 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("12.3")) // Contains period
42 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("12-34")) // Contains hyphen
43 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("1234!")) // Contains special character
44 | }
45 |
46 | @Test
47 | fun `test invalid PINs - all same digit`() {
48 | // PINs with all the same digit
49 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("1111")) // All 1's
50 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("0000")) // All 0's
51 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("99999")) // All 9's
52 | }
53 |
54 | @Test
55 | fun `test invalid PINs - sequential ascending`() {
56 | // PINs with sequential ascending digits
57 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("1234")) // Sequential ascending
58 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("4567")) // Sequential ascending
59 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("01234")) // Sequential ascending
60 | }
61 |
62 | @Test
63 | fun `test invalid PINs - sequential descending`() {
64 | // PINs with sequential descending digits
65 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("4321")) // Sequential descending
66 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("9876")) // Sequential descending
67 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("54321")) // Sequential descending
68 | }
69 |
70 | @Test
71 | fun `test invalid PINs - black list`() {
72 | // PINs with sequential descending digits
73 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("1212"))
74 | assertFalse(pinStrengthCheckUseCase.isPinStrongEnough("6969"))
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/test/resources/red.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/app/src/test/resources/red.jpg
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | alias(libs.plugins.kotlin.compose) apply false
6 | }
--------------------------------------------------------------------------------
/docs/SnapSafe Attack Vectors.md:
--------------------------------------------------------------------------------
1 | # SnapSafe Attack Vectors
2 |
3 | ##
4 |
5 | ## Casual attacker, Unlocked Phone
6 |
7 | This could be anything from a child to an authority figure with hard power over you who has your device. Your user files
8 | are decrypted, but still protected by normal Linux permissions, so this attacker will have no access to them.
9 |
10 | SnapSafe has several bulwarks to protect you here:
11 |
12 | 1. If the app is close, opening it will present the attacker with a PIN screen.
13 | 1. Attempts at guessing the PIN are throttled with an exponential back off
14 | 2. A strong PIN is required, no straight sequences or mono-digit PINs
15 | 3. After 10 failed attempts, all data is wiped
16 | 4. Number of failed attempts and current back-off timeout are both persisted over app restart and device restart
17 | 2. If the app is open in the background
18 | 1. The app will not show a preview in the Task Switcher
19 | 2. The app has a session timeout, so it's possible that when the attacker attempts to foreground the app, they will
20 | be required to enter a PIN
21 | 3. If the session is still valid, the attacker will gain full access to the app, but only for the remainder of the
22 | session. After which they will be required to re-authenticate to continue exploiting it.
23 |
24 | ## Determined attacker, Unlocked Phone, Unexploited
25 |
26 | The attacker has plugged you device into a computer. Their software uses only "normal" means such as ADB to pull data
27 | off of the phone.
28 |
29 | SnapSafe protects against this:
30 |
31 | 1. No data from SnapSafe will be able to be copied off of the device, Nothing is stored in public directories, and
32 | SnapSafe restricts all forms of Backups, so no normal tools will be allowed to create a backup of SnapSafe's private
33 | data.
34 |
35 | ## Determined attacker, Unlocked Phone, Exploited, Filesystem permissions bypassed
36 |
37 | The attacker has plugged you device into a computer. Their software succeeds in exploiting vulnerabilities present on
38 | this particular device. If they manage to compromise the OS's permissions and restrictions, they may gain access to
39 | SnapSafe's private files.
40 |
41 | SnapSafe protects against this:
42 |
43 | 1. The PIN is hashed using Argon2, a brute force resistant cryptographically secure hashing algorithm. This is then
44 | encrypted using a Hardware backed key. What would be captured on disk is a strongly encrypted blob who's key resides
45 | in the Hardware Key store.
46 | 2. All sensitive files are encrypted using the primary Data Encryption Key, a 256 bit AES key.
47 | 1. In the default mode, this DEK is wrapped using a KEK in the Hardware Keystore (TEE or SE), and saved to disk. So
48 | what would be captured is a strongly encrypted blob.
49 | 2. In the ephemeral mode, this DEK is only ever derived in memory, and is never written to disk
50 |
51 | ## Determined attacker, Unlocked Phone, Exploited, Memory access bypassed, app is resident in memory
52 |
53 | The attacker has plugged you device into a computer. Their software succeeds in exploiting vulnerabilities present on
54 | this particular device. If they manage to compromise the OS's permissions and restrictions, they may gain access to some
55 | or all of SnapSafe's memory.
56 |
57 | **Case A: The app is in memory, but the session has expired**
58 |
59 | There is no sensitive data in memory. The encryption key, photos, and thumbnails have all been evicted from memory.
60 |
61 | **Case B: The app is in memory, with a valid session**
62 |
63 | This is the worst case scenario, but we can still provide some protection. The DEK is stored in a Shared Key, where it
64 | is split with an XOR cipher, and the two halves are only brought back together briefly when a crypto op is actually
65 | being executed.
--------------------------------------------------------------------------------
/docs/SnapSafe Related Incidents.md:
--------------------------------------------------------------------------------
1 | # SnapSafe's Inspirational Incidents
2 |
3 | These are some of the historical incidents that inspired the need for SnapSafe.
4 |
5 | ### Apple continued storing long deleted photos
6 |
7 | [https://www.theverge.com/2024/5/20/24160983/apple-iphone-ipad-deleted-photo-reappear-bug-ios-17-5-1](https://www.theverge.com/2024/5/20/24160983/apple-iphone-ipad-deleted-photo-reappear-bug-ios-17-5-1)
8 |
9 | ### Google Account Termination Over Medical Photos
10 |
11 | [https://www.theguardian.com/technology/2022/aug/22/google-csam-account-blocked](https://www.theguardian.com/technology/2022/aug/22/google-csam-account-blocked)
12 |
13 | ### Brown University doctor denied entry to U.S. because of deleted photos on her phone
14 |
15 | [https://www.cbsnews.com/boston/news/brown-university-rasha-alawieh-denied-entry-us-hezbollah/](https://www.cbsnews.com/boston/news/brown-university-rasha-alawieh-denied-entry-us-hezbollah/)
16 |
17 | [https://www.cnn.com/2025/03/25/us/migrant-deportation-trump-evidence](https://www.cnn.com/2025/03/25/us/migrant-deportation-trump-evidence)
18 |
19 | ### Police browse women's nude photos at traffic stop
20 |
21 | [https://apnews.com/article/missouri-officers-indicted-nude-photos-7f489530022d7b7664c9567ecbd4cb9b](https://apnews.com/article/missouri-officers-indicted-nude-photos-7f489530022d7b7664c9567ecbd4cb9b)
22 |
23 | ### Samsung Secure Folder Bug Exposes Private Media
24 |
25 | [https://www.thesun.co.uk/tech/33595162/photos-videos-secure-folder-samsung-phone-bug-flaw/](https://www.thesun.co.uk/tech/33595162/photos-videos-secure-folder-samsung-phone-bug-flaw/)
26 |
27 | ### Halsey's Private Photos Viewed Without Consent
28 |
29 | [https://pagesix.com/2024/10/30/celebrity-news/halsey-reveals-music-exec-looked-at-their-nude-photos-without-consent/](https://pagesix.com/2024/10/30/celebrity-news/halsey-reveals-music-exec-looked-at-their-nude-photos-without-consent/)
30 |
31 | ### iOS 18's 'Enhanced Visual Search' Shares Photos by Default
32 |
33 | [https://nypost.com/2024/12/31/tech/sneaky-ios-18-setting-shares-photos-with-apple-how-to-turn-it-off/](https://nypost.com/2024/12/31/tech/sneaky-ios-18-setting-shares-photos-with-apple-how-to-turn-it-off/)
34 |
35 | -----------------
36 |
37 | ### Can't share photos from Google Photos "locked folder"
38 |
39 | [https://support.google.com/photos/thread/189913472/google-photos-locked-folder?hl=en](https://support.google.com/photos/thread/189913472/google-photos-locked-folder?hl=en)
40 |
41 | ### Google's "local folder" now uploads to the cloud
42 |
43 | [https://www.neowin.net/news/google-photos-locked-folder-can-now-be-backed-up-and-shared-across-devices/](https://www.neowin.net/news/google-photos-locked-folder-can-now-be-backed-up-and-shared-across-devices/)
--------------------------------------------------------------------------------
/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | package_name("com.darkrockstudios.app.securecamera")
2 | json_key_file("play-store-config.json")
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | default_platform(:android)
2 |
3 | platform :android do
4 | desc "Deploy to Play Store"
5 | lane :deploy do
6 | upload_to_play_store(
7 | track: 'production',
8 | aab: 'app/build/outputs/bundle/fullRelease/app-full-release.aab',
9 | skip_upload_metadata: false,
10 | skip_upload_images: false,
11 | skip_upload_screenshots: false,
12 | release_status: 'completed'
13 | )
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/fastlane/README.md:
--------------------------------------------------------------------------------
1 | # FastLane Setup for SnapSafe
2 |
3 | This directory contains the FastLane configuration for automating the deployment of SnapSafe to Google Play.
4 |
5 | ## Directory Structure
6 |
7 | - `Fastfile`: Defines the deployment lanes for FastLane
8 | - `Appfile`: Contains app-specific configuration (package name, service account key path)
9 | - `metadata/`: Contains app metadata for Google Play
10 | - `android/`: Platform-specific metadata
11 | - `en-US/`: English (US) metadata
12 | - `title.txt`: App title
13 | - `short_description.txt`: Short app description
14 | - `full_description.txt`: Full app description
15 | - `images/`: App screenshots and images
16 | - `phoneScreenshots/`: Screenshots for phones
17 |
18 | ## How to Use
19 |
20 | ### Prerequisites
21 |
22 | 1. Install FastLane:
23 | ```bash
24 | gem install fastlane
25 | ```
26 |
27 | 2. Set up a Google Play service account and download the JSON key file
28 | - Place the key file in the fastlane directory as `play-store-config.json`
29 | - Make sure the service account has the necessary permissions in the Google Play Console
30 |
31 | ### Deploying to Google Play
32 |
33 | To deploy a new version to Google Play:
34 |
35 | 1. Update the version information in `gradle/libs.versions.toml`
36 | 2. Build the release AAB:
37 | ```bash
38 | ./gradlew bundleRelease
39 | ```
40 | 3. Run the deploy lane from the project root directory:
41 | ```bash
42 | fastlane deploy
43 | ```
44 |
45 | > **Note**: The Fastfile is configured to look for the AAB file at `app/build/outputs/bundle/release/app-release.aab`
46 | > relative to the project root directory. Make sure you run the `fastlane deploy` command from the project root, not from
47 | > the fastlane directory.
48 |
49 | ### Automated Deployment
50 |
51 | The project uses GitHub Actions to automatically build and deploy new releases to Google Play when a tag with the format
52 | `v*` (e.g., `v1.0.0`) is pushed. See the [GitHub Actions workflow documentation](../.github/workflows/README.md) for
53 | details.
54 |
55 | ## Customizing Metadata
56 |
57 | To update the app metadata:
58 |
59 | 1. Edit the files in the `metadata/android/en-US/` directory
60 | 2. Add screenshots to the `metadata/android/en-US/images/phoneScreenshots/` directory
61 | 3. Run the deploy lane to upload the updated metadata to Google Play
62 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/full_description.txt:
--------------------------------------------------------------------------------
1 | SnapSafe ist eine Kamera-App, die jedes Pixel – und jedes einzelne Byte – genau dort lässt, wo es hingehört: auf DEINEM Gerät.
2 |
3 | Warum SnapSafe?
4 | Wir speichern Fotos lokal, verschlüsseln alles im privaten App-Speicher, lassen DICH entscheiden, ob GPS-Tags hinzugefügt werden, und entfernen automatisch verräterische Metadaten.
5 |
6 | Frei und Open Source – für immer.
7 |
8 | Wir telefonieren NIEMALS nach Hause oder kontaktieren Server, sammeln keine Analysedaten oder Nutzungsstatistiken, blenden keine Werbung oder Tracker ein und lesen keine Dateien außerhalb unserer Sandbox.
9 | Hauptfunktionen
10 |
11 | • Zero-Leak-Design – Die Manifestdatei verzichtet bewusst auf android.permission.INTERNET; nichts verlässt dein Gerät.
12 | • Vollständig verschlüsselt – Aufnahmen werden mit starker Verschlüsselung im App-Privatspeicher abgelegt.
13 | • Metadata Scrub-A-Dub – EXIF- und andere Identifikatoren werden sofort gelöscht, sobald du „Teilen“ drückst.
14 | • Sicheres Teilen – Wenn du teilst, übergeben wir die Datei direkt an das native Android-Sharesheet – ohne Umwege.
15 | • Anti-Doxing – Gesichter werden vor dem Teilen automatisch unscharf gemacht.
16 | • PIN-geschützte Galerie – Eine separate PIN schützt deine Fotos vor neugierigen Blicken.
17 | • Granularer Standort – Füge grobe, präzise oder gar keine Standortdaten hinzu – du hast die Wahl.
18 | • 100 % Open Source – Prüfbarer Code, offen einsehbar.
19 | • Poison Pill – Lege eine spezielle PIN fest: Sie wirkt normal, löscht aber heimlich deine vorhandenen Fotos.
20 | • Tarnfotos – Wähle harmlose Tarnfotos, die beim Aktivieren der Poison Pill erhalten bleiben, damit deine Galerie nicht verdächtig leer aussieht.
21 |
22 | Datenschutzerklärung: Wir erfassen nichts.
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/de-DE/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/de-DE/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/images/phoneScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/de-DE/images/phoneScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/images/phoneScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/de-DE/images/phoneScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/images/phoneScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/de-DE/images/phoneScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/images/phoneScreenshots/4_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/de-DE/images/phoneScreenshots/4_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/images/phoneScreenshots/5_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/de-DE/images/phoneScreenshots/5_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/images/sevenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/de-DE/images/sevenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/images/sevenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/de-DE/images/sevenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/images/sevenInchScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/de-DE/images/sevenInchScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/images/tenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/de-DE/images/tenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/images/tenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/de-DE/images/tenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/short_description.txt:
--------------------------------------------------------------------------------
1 | Mach sichere Fotos von allem.
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/title.txt:
--------------------------------------------------------------------------------
1 | SnapTresor
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/10.txt:
--------------------------------------------------------------------------------
1 | - Added Face Blurring tool
2 | - Translated into more languages
3 | - Sharing no longer writes unencrypted bits to disk
4 | - Nicer UI animations
5 | - Lots of refactoring
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/12.txt:
--------------------------------------------------------------------------------
1 | Our first production ready release.
2 | All data from the Beta will be wiped!
3 | - Completely redesigned cryptography!
4 | - Security Hardening throughout the app
5 | - Import photos from other apps into the Safe
6 | - UI Cleanup and improvements
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/13.txt:
--------------------------------------------------------------------------------
1 | Our first production ready release.
2 | All data from the Beta will be wiped!
3 | - Completely redesigned cryptography!
4 | - Security Hardening throughout the app
5 | - Import photos from other apps into the Safe
6 | - UI Cleanup and improvements
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/14.txt:
--------------------------------------------------------------------------------
1 | - Improved PIN hashing security
2 | - Camera Preview no longer crops the frame
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/15.txt:
--------------------------------------------------------------------------------
1 | - Fixed crash in Gallery after photo delete
2 | - Added Session Watcher
3 | Sessions will now be terminated even if the app is backgrounded, evicting all sensitive data from memory.
4 | - Session Timeout now only starts ticking down when the app is backgrounded
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/16.txt:
--------------------------------------------------------------------------------
1 | - Fixed KeyStore crash on pre-API 31 devices
2 | - Fixed UI not allowing for 3 button system controls
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/17.txt:
--------------------------------------------------------------------------------
1 | - Fix a couple crashes seen in the wild
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/3.txt:
--------------------------------------------------------------------------------
1 | - Added Security Reset option
2 | - Added Poison Pill: A second PIN that when entered, wipes your data
3 | - Added weak PIN check
4 | - Improved session timeout handling
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/4.txt:
--------------------------------------------------------------------------------
1 | - Added Decoy Photo system
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/8.txt:
--------------------------------------------------------------------------------
1 | - Rewrote the Camera
2 | + Added pinch to zoom, tap to focus, Level
3 | - Rewrote Photo Viewer
4 | - Fixed some bugs
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | SnapSafe is a camera app that keeps every pixel—and every byte of data—exactly where it belongs: on YOUR device.
2 |
3 | Why SnapSafe?
4 | We capture photos locally, encrypt everything in private storage, let YOU decide if GPS tags are added, and strip out tell‑tale metadata automatically.
5 |
6 | Free and Open Source, forever.
7 |
8 | We NEVER phone home or talk to servers, slurp analytics or usage stats, sprinkle ads or trackers in the code, or read files outside our sandbox.
9 |
10 | Key Features:
11 | • Zero‑Leak Design – The manifest skips android.permission.INTERNET; nothing leaves your device.
12 | • Fully Encrypted – Shots are written to app‑private storage using strong encryption.
13 | • Metadata Scrub‑A‑Dub – EXIF and other identifiers are wiped the instant you hit Share.
14 | • Secure Sharing – When you do share, we hand the file off via Android's native share sheet—no detours.
15 | • Anti-Doxing – Automatically Blur faces before sharing photos
16 | • PIN‑Locked Gallery – A separate PIN stands between curious thumbs and your photos.
17 | • Granular Location – Add coarse, fine, or zero location data—your call.
18 | • 100% Open Source – Auditable code in plain sight.
19 | • Poison Pill – Set a special PIN, that when entered, appears to work normally but actually deletes your existing
20 | photos.
21 | • Decoy Photos – Select innocuous decoy photos, these will be preserved when your Poison Pill is activated. That way
22 | your Gallery is not suspiciously empty.
23 |
24 | Privacy Policy: We collect nothing.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/sevenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/en-US/images/sevenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/sevenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/en-US/images/sevenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/sevenInchScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/en-US/images/sevenInchScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/tenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/en-US/images/tenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/tenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/en-US/images/tenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Snap pics of anything, safely.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | SnapSafe
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/full_description.txt:
--------------------------------------------------------------------------------
1 | SnapSafe es una aplicación de cámara que mantiene cada píxel—y cada byte de datos—exactamente donde pertenece: en TU dispositivo.
2 |
3 | ¿Por qué SnapSafe?
4 | Capturamos fotos localmente, ciframos todo en almacenamiento privado, dejamos que TÚ decidas si se añaden etiquetas GPS y eliminamos automáticamente los metadatos reveladores.
5 |
6 | Gratis y de código abierto, para siempre.
7 |
8 | NUNCA llamamos a casa ni nos conectamos a servidores, no recopilamos analíticas ni estadísticas de uso, no incluimos anuncios ni rastreadores en el código, ni leemos archivos fuera de nuestro sandbox.
9 |
10 | Funciones clave:
11 | • Diseño sin fugas – El manifiesto omite android.permission.INTERNET; nada sale de tu dispositivo.
12 | • Totalmente cifrado – Las capturas se guardan en el almacenamiento privado de la app usando cifrado robusto.
13 | • Limpieza de metadatos – El EXIF y otros identificadores se eliminan en cuanto pulsas Compartir.
14 | • Compartir seguro – Cuando compartes, entregamos el archivo mediante la hoja de compartir nativa de Android—sin desvíos.
15 | • Anti-doxing – Difumina automáticamente las caras antes de compartir fotos.
16 | • Galería protegida con PIN – Un PIN independiente se interpone entre pulgares curiosos y tus fotos.
17 | • Ubicación granular – Añade datos de ubicación gruesa, precisa o ninguno—tú eliges.
18 | • 100 % de código abierto – Código auditable a la vista de todos.
19 | • Píldora venenosa – Configura un PIN especial que, al introducirse, parece funcionar con normalidad pero borra tus fotos existentes.
20 | • Fotos señuelo – Selecciona fotos inocuas como señuelo; se conservarán cuando se active la Píldora venenosa, para que tu galería no quede sospechosamente vacía.
21 |
22 | Política de privacidad: No recogemos nada.
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/es-ES/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/es-ES/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/images/phoneScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/es-ES/images/phoneScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/images/phoneScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/es-ES/images/phoneScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/images/phoneScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/es-ES/images/phoneScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/images/phoneScreenshots/4_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/es-ES/images/phoneScreenshots/4_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/images/phoneScreenshots/5_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/es-ES/images/phoneScreenshots/5_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/images/sevenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/es-ES/images/sevenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/images/sevenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/es-ES/images/sevenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/images/sevenInchScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/es-ES/images/sevenInchScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/images/tenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/es-ES/images/tenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/images/tenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/es-ES/images/tenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/short_description.txt:
--------------------------------------------------------------------------------
1 | Haz fotos de cualquier cosa, de forma segura.
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/title.txt:
--------------------------------------------------------------------------------
1 | SnapCofre
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/full_description.txt:
--------------------------------------------------------------------------------
1 | SnapSafe est une appli appareil photo qui garde chaque pixel — et chaque octet de données — exactement là où ils doivent être : sur VOTRE appareil.
2 |
3 | Pourquoi SnapSafe ?
4 | Nous prenons les photos localement, chiffrons tout dans le stockage privé, vous laissons décider si des balises GPS sont ajoutées et supprimons automatiquement les métadonnées révélatrices.
5 |
6 | Gratuit et Open Source, pour toujours.
7 |
8 | Nous ne téléphonons JAMAIS à la maison ni ne contactons de serveurs, n’aspirons aucune statistique d’utilisation, n’insérons ni pubs ni traqueurs dans le code, et ne lisons pas les fichiers en dehors de notre bac à sable.
9 |
10 | Fonctionnalités clés :
11 | • Conception « zéro fuite » – Le manifeste exclut android.permission.INTERNET ; rien ne quitte votre appareil.
12 | • Entièrement chiffré – Les clichés sont enregistrés dans le stockage privé de l’appli à l’aide d’un chiffrement robuste.
13 | • Nettoyage des métadonnées – Les données EXIF et autres identifiants sont effacés dès que vous appuyez sur Partager.
14 | • Partage sécurisé – Lorsque vous partagez, nous remettons le fichier via la feuille de partage native d’Android — pas de détours.
15 | • Anti-doxing – Floutez automatiquement les visages avant de partager des photos.
16 | • Galerie verrouillée par code PIN – Un code PIN distinct se dresse entre des pouces curieux et vos photos.
17 | • Localisation granulaire – Ajoutez une localisation approximative, précise ou aucune — c’est vous qui décidez.
18 | • 100 % Open Source – Code vérifiable à la vue de tous.
19 | • Pilule empoisonnée – Définissez un code PIN spécial qui, une fois saisi, paraît fonctionner normalement mais supprime vos photos existantes.
20 | • Photos leurres – Sélectionnez des clichés anodins ; ils seront préservés lorsque la pilule empoisonnée est activée, afin que votre galerie ne soit pas suspectement vide.
21 |
22 | Politique de confidentialité : nous ne collectons rien.
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/fr-FR/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/fr-FR/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/images/phoneScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/fr-FR/images/phoneScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/images/phoneScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/fr-FR/images/phoneScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/images/phoneScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/fr-FR/images/phoneScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/images/phoneScreenshots/4_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/fr-FR/images/phoneScreenshots/4_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/images/phoneScreenshots/5_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/fr-FR/images/phoneScreenshots/5_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/images/sevenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/fr-FR/images/sevenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/images/sevenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/fr-FR/images/sevenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/images/sevenInchScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/fr-FR/images/sevenInchScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/images/tenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/fr-FR/images/tenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/images/tenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/fr-FR/images/tenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/short_description.txt:
--------------------------------------------------------------------------------
1 | Prenez des photos de tout, en toute sécurité.
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/title.txt:
--------------------------------------------------------------------------------
1 | SnapCoffre
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/full_description.txt:
--------------------------------------------------------------------------------
1 | SnapSafe è un'app fotocamera che mantiene ogni pixel—e ogni byte di dati—esattamente dove deve stare: sul TUO dispositivo.
2 |
3 | Perché SnapSafe?
4 | Scattiamo foto localmente, cifriamo tutto nell'archivio privato, lasciamo decidere a TE se aggiungere i tag GPS e rimuoviamo automaticamente i metadati rivelatori.
5 |
6 | Gratuito e Open Source, per sempre.
7 |
8 | NON comunichiamo mai con server esterni, non inviamo analytics o statistiche d’uso, non inseriamo pubblicità o tracker nel codice e non leggiamo file fuori dal nostro sandbox.
9 |
10 | Funzionalità principali:
11 | • Progettazione Zero-Leak – Il manifest non include android.permission.INTERNET; nulla lascia il tuo dispositivo.
12 | • Crittografia completa – Gli scatti vengono salvati nell'archivio privato dell'app con forte crittografia.
13 | • Metadata Scrub-A-Dub – I dati EXIF e altri identificatori vengono eliminati non appena premi Condividi.
14 | • Condivisione sicura – Quando condividi, passiamo il file tramite il foglio di condivisione nativo di Android—senza deviazioni.
15 | • Anti-Doxing – Sfoca automaticamente i volti prima di condividere le foto.
16 | • Galleria protetta da PIN – Un PIN separato si frappone tra mani curiose e le tue foto.
17 | • Posizione granulare – Aggiungi dati di posizione approssimativi, precisi o nessuno—decidi tu.
18 | • 100% Open Source – Codice verificabile alla luce del sole.
19 | • Pillola avvelenata – Imposta un PIN speciale che, una volta inserito, sembra funzionare normalmente ma in realtà elimina le tue foto esistenti.
20 | • Foto esca – Seleziona foto innocue da usare come esca; saranno conservate quando la Pillola avvelenata si attiva, così la tua Galleria non risulterà sospettosamente vuota.
21 |
22 | Informativa sulla privacy: non raccogliamo nulla.
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/it-IT/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/it-IT/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/images/phoneScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/it-IT/images/phoneScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/images/phoneScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/it-IT/images/phoneScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/images/phoneScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/it-IT/images/phoneScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/images/phoneScreenshots/4_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/it-IT/images/phoneScreenshots/4_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/images/phoneScreenshots/5_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/it-IT/images/phoneScreenshots/5_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/images/sevenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/it-IT/images/sevenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/images/sevenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/it-IT/images/sevenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/images/sevenInchScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/it-IT/images/sevenInchScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/images/tenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/it-IT/images/tenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/images/tenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/it-IT/images/tenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/short_description.txt:
--------------------------------------------------------------------------------
1 | Scatta foto di tutto, in sicurezza.
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/title.txt:
--------------------------------------------------------------------------------
1 | SnapCassaforte
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/full_description.txt:
--------------------------------------------------------------------------------
1 | SnapSafe é um app de câmera que mantém cada pixel — e cada byte de dados — exatamente onde deve ficar: no SEU dispositivo.
2 |
3 | Por que usar o SnapSafe?
4 | Capturamos as fotos localmente, criptografamos tudo no armazenamento privado, deixamos VOCÊ decidir se as tags de GPS serão adicionadas e removemos automaticamente metadados reveladores.
5 |
6 | Livre e de código aberto, para sempre.
7 |
8 | NUNCA telefonamos para casa ou conversamos com servidores, não coletamos análises nem estatísticas de uso, não inserimos anúncios ou rastreadores no código, nem lemos arquivos fora do nosso sandbox.
9 |
10 | Principais recursos:
11 | • Design Sem Vazamentos – O manifesto dispensa android.permission.INTERNET; nada sai do seu dispositivo.
12 | • Totalmente Criptografado – As fotos são gravadas no armazenamento privado do app com criptografia forte.
13 | • Limpeza de Metadados – EXIF e outros identificadores são apagados assim que você toca em Compartilhar.
14 | • Compartilhamento Seguro – Ao compartilhar, entregamos o arquivo via folha de compartilhamento nativa do Android — sem desvios.
15 | • Anti-Doxing – Desfoca rostos automaticamente antes de compartilhar fotos.
16 | • Galeria Protegida por PIN – Um PIN separado impede dedos curiosos de acessarem suas fotos.
17 | • Localização Granular – Adicione localização grosseira, precisa ou nenhuma — você decide.
18 | • 100% Código Aberto – Código auditável à vista de todos.
19 | • Pílula de Veneno – Defina um PIN especial que, ao ser inserido, parece funcionar normalmente mas apaga suas fotos existentes.
20 | • Fotos-Isca – Selecione fotos inocentes; elas serão preservadas quando a Pílula de Veneno for ativada, para que sua galeria não fique suspeitamente vazia.
21 |
22 | Política de Privacidade: não coletamos nada.
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/pt-BR/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/pt-BR/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/images/phoneScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/pt-BR/images/phoneScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/images/phoneScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/pt-BR/images/phoneScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/images/phoneScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/pt-BR/images/phoneScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/images/phoneScreenshots/4_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/pt-BR/images/phoneScreenshots/4_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/images/phoneScreenshots/5_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/pt-BR/images/phoneScreenshots/5_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/images/sevenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/pt-BR/images/sevenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/images/sevenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/pt-BR/images/sevenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/images/sevenInchScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/pt-BR/images/sevenInchScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/images/tenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/pt-BR/images/tenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/images/tenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/pt-BR/images/tenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/short_description.txt:
--------------------------------------------------------------------------------
1 | Tire fotos de qualquer coisa, com segurança.
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-BR/title.txt:
--------------------------------------------------------------------------------
1 | SnapCofre
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/full_description.txt:
--------------------------------------------------------------------------------
1 | SnapSafe — це застосунок камери, що зберігає кожен піксель і кожен байт даних саме там, де їм і місце — на ВАШОМУ пристрої.
2 |
3 | Чому SnapSafe?
4 | Ми робимо знімки локально, шифруємо все в приватному сховищі, дозволяємо ВАМ визначати, чи додавати GPS-мітки, і автоматично видаляємо всі розпізнавальні метадані.
5 |
6 | Безкоштовний і з відкритим кодом, назавжди.
7 |
8 | Ми НІКОЛИ не зв’язуємося з віддаленими серверами, не збираємо аналітику чи статистику, не додаємо рекламу чи трекери в код і не читаємо файли поза нашою пісочницею.
9 |
10 | Ключові можливості:
11 | • Zero-Leak Design – у маніфесті відсутній android.permission.INTERNET; жоден байт не покидає ваш пристрій.
12 | • Повне шифрування – знімки записуються у приватне сховище застосунку з використанням надійного шифрування.
13 | • Metadata Scrub-A-Dub – EXIF та інші ідентифікатори стираються миттєво після натискання «Поділитися».
14 | • Безпечний обмін – коли ви ділитеся фото, ми передаємо файл через вбудований аркуш спільного доступу Android — без обхідних шляхів.
15 | • Anti-Doxing – автоматичне розмиття облич перед публікацією фото.
16 | • Галерея, захищена PIN-кодом – окремий PIN-код відділяє допитливі пальці від ваших фото.
17 | • Гранульована геолокація – додавайте грубі, точні або нульові дані про місце зйомки — на ваш розсуд.
18 | • 100 % Open Source – відкритий код, доступний для аудиту.
19 | • Poison Pill – встановіть особливий PIN-код; на вигляд усе працює як звично, але насправді цей PIN видаляє всі ваші фото.
20 | • Фальшиві фото – оберіть безневинні підставні знімки; вони збережуться після активації Poison Pill, щоб Галерея не виглядала підозріло порожньою.
21 |
22 | Політика конфіденційності: ми нічого не збираємо.
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/uk/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/uk/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/images/phoneScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/uk/images/phoneScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/images/phoneScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/uk/images/phoneScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/images/phoneScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/uk/images/phoneScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/images/phoneScreenshots/4_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/uk/images/phoneScreenshots/4_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/images/phoneScreenshots/5_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/uk/images/phoneScreenshots/5_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/images/sevenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/uk/images/sevenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/images/sevenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/uk/images/sevenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/images/sevenInchScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/uk/images/sevenInchScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/images/tenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/uk/images/tenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/images/tenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/uk/images/tenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/short_description.txt:
--------------------------------------------------------------------------------
1 | Знімайте що завгодно безпечно.
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk/title.txt:
--------------------------------------------------------------------------------
1 | СнапСейф
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/full_description.txt:
--------------------------------------------------------------------------------
1 | SnapSafe 是一款相机应用,可将每个像素——以及每一字节数据——都严格保存在一个地方:您的设备。
2 |
3 | 为什么选择 SnapSafe?
4 | 我们本地拍摄照片,将所有内容加密存储在私有空间,由您决定是否添加 GPS 标签,并自动去除所有元数据痕迹。
5 |
6 | 永久免费且开源。
7 |
8 | 我们绝不会回传数据或连接任何服务器,不会收集分析或使用统计,不在代码中植入广告或跟踪器,也不会读取沙盒之外的文件。
9 |
10 | 主要功能:
11 | • 零泄漏设计——清单省略 android.permission.INTERNET;任何数据都不会离开您的设备。
12 | • 全程加密——照片使用强加密写入应用私有存储。
13 | • 元数据净化——一旦您点击“分享”,EXIF 等标识立即被清除。
14 | • 安全分享——通过 Android 原生分享面板直接传递文件,无需绕路。
15 | • 反人肉搜索——分享前自动模糊人脸。
16 | • PIN 加锁图库——另一组 PIN 码阻挡好奇的手指。
17 | • 细粒度定位——可添加粗略、精确或无位置信息,由您决定。
18 | • 100% 开源——代码完全可审计。
19 | • 毒丸模式——设置特殊 PIN,输入后看似正常,却会删除现有照片。
20 | • 诱饵照片——选择无害诱饵照片,毒丸触发时这些照片会被保留,使图库不至于空空如也。
21 |
22 | 隐私政策:我们不收集任何数据。
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/zh-CN/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/zh-CN/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/images/phoneScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/zh-CN/images/phoneScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/images/phoneScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/zh-CN/images/phoneScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/images/phoneScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/zh-CN/images/phoneScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/images/phoneScreenshots/4_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/zh-CN/images/phoneScreenshots/4_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/images/phoneScreenshots/5_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/zh-CN/images/phoneScreenshots/5_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/images/sevenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/zh-CN/images/sevenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/images/sevenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/zh-CN/images/sevenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/images/sevenInchScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/zh-CN/images/sevenInchScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/images/tenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/zh-CN/images/tenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/images/tenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/fastlane/metadata/android/zh-CN/images/tenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/short_description.txt:
--------------------------------------------------------------------------------
1 | 安全拍下任何东西。
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-CN/title.txt:
--------------------------------------------------------------------------------
1 | 拍安箱
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SecureCamera/SecureCameraAndroid/407fa446b0056b11ef943b926b0418ef0bccc66d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Apr 16 15:13:31 PDT 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | maven { url = uri("https://jitpack.io") }
20 | }
21 | }
22 |
23 | rootProject.name = "SecureCamera"
24 | include(":app")
25 |
--------------------------------------------------------------------------------