├── app ├── .gitignore ├── src │ ├── androidTest │ │ ├── assets │ │ │ ├── invalid │ │ │ │ ├── emptyzip.zip │ │ │ │ ├── emptynote.7z │ │ │ │ ├── broken_zipcrypto.zip │ │ │ │ └── unencrypted_note.zip │ │ │ ├── 4notes.aeszip │ │ │ ├── subdirs.aeszip │ │ │ ├── compression │ │ │ │ ├── 100a.txt │ │ │ │ ├── 100a_flat.aeszip │ │ │ │ ├── 100a_lzma.aeszip │ │ │ │ ├── 100a_ppmd.aeszip │ │ │ │ ├── 100a_bzip2.aeszip │ │ │ │ ├── 100a_deflate.aeszip │ │ │ │ ├── 100a_deflate64.aeszip │ │ │ │ ├── 100a_deflate_fast.aeszip │ │ │ │ └── 100a_deflate_ultra.aeszip │ │ │ ├── singlenote.aeszip │ │ │ ├── twopasswords.ZIP │ │ │ └── 4passwords_subdirs.aeszip │ │ └── java │ │ │ └── com │ │ │ └── ditronic │ │ │ └── securezipnotes │ │ │ ├── contrib │ │ │ └── NonFatalAbortTree.kt │ │ │ ├── robotpattern │ │ │ └── Preconditions.kt │ │ │ ├── tests │ │ │ ├── MultiPasswordTests.kt │ │ │ └── SetupTests.kt │ │ │ ├── onlinetests │ │ │ └── SyncTests.kt │ │ │ └── testutils │ │ │ └── TestUtils.kt │ ├── main │ │ ├── ic_launcher-web.png │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── xml │ │ │ │ ├── provider_paths.xml │ │ │ │ └── backup_rules.xml │ │ │ ├── values-v21 │ │ │ │ └── styles.xml │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ ├── styles.xml │ │ │ │ └── strings.xml │ │ │ ├── menu │ │ │ │ ├── menu_noteselect_longclick.xml │ │ │ │ ├── menu_noteedit.xml │ │ │ │ └── menu_noteselect.xml │ │ │ ├── layout │ │ │ │ ├── common_toolbar.xml │ │ │ │ ├── cardview.xml │ │ │ │ ├── activity_note_edit.xml │ │ │ │ ├── activity_password_confirm.xml │ │ │ │ ├── activity_new_password.xml │ │ │ │ └── activity_main.xml │ │ │ └── drawable │ │ │ │ ├── ic_baseline_add_24px.xml │ │ │ │ ├── ic_baseline_warning_24px.xml │ │ │ │ ├── ic_baseline_arrow_forward_24px.xml │ │ │ │ ├── ic_baseline_cloud_download_24px.xml │ │ │ │ ├── ic_dropbox.xml │ │ │ │ └── ic_drive.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── ditronic │ │ │ │ └── securezipnotes │ │ │ │ ├── app │ │ │ │ ├── AppEnvironment.kt │ │ │ │ ├── ApplicationExt.kt │ │ │ │ └── ViewModelFactory.kt │ │ │ │ ├── util │ │ │ │ ├── OnThrottleClickListener.kt │ │ │ │ ├── OnThrottleItemClickListener.kt │ │ │ │ ├── BannerAds.kt │ │ │ │ └── Boast.kt │ │ │ │ ├── noteedit │ │ │ │ ├── NoteEditViewModel.kt │ │ │ │ ├── PlainEditText.kt │ │ │ │ └── NoteEditUI.kt │ │ │ │ ├── dialogs │ │ │ │ ├── DeleteDialog.kt │ │ │ │ ├── ShortLifeDialogFragment.kt │ │ │ │ ├── RenameFileDialog.kt │ │ │ │ └── PwDialog.kt │ │ │ │ ├── logging │ │ │ │ └── CrashlyticsTree.kt │ │ │ │ ├── zip │ │ │ │ ├── ZipUtil.kt │ │ │ │ └── NotesImport.kt │ │ │ │ ├── noteselect │ │ │ │ └── NoteSelectAdapter.kt │ │ │ │ ├── menu │ │ │ │ └── MenuOptions.kt │ │ │ │ ├── onboarding │ │ │ │ ├── NewPasswordActivity.kt │ │ │ │ └── PasswordConfirmActivity.kt │ │ │ │ └── password │ │ │ │ └── PwManager.kt │ │ └── AndroidManifest.xml │ ├── debug │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ ├── release │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ └── test │ │ └── java │ │ └── com │ │ └── ditronic │ │ └── securezipnotes │ │ └── ExampleUnitTest.java ├── lint.xml ├── proguard-rules.pro ├── google-services.json └── build.gradle ├── zip4j ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── net │ │ └── lingala │ │ └── zip4j │ │ ├── io │ │ ├── BaseInputStream.java │ │ ├── ZipOutputStream.java │ │ ├── BaseOutputStream.java │ │ ├── ZipInputStream.java │ │ └── DeflaterOutputStream.java │ │ ├── crypto │ │ ├── IDecrypter.java │ │ ├── IEncrypter.java │ │ ├── PBKDF2 │ │ │ ├── PRF.java │ │ │ ├── PBKDF2HexFormatter.java │ │ │ ├── BinTools.java │ │ │ ├── PBKDF2Parameters.java │ │ │ └── MacBasedPRF.java │ │ ├── engine │ │ │ └── ZipCryptoEngine.java │ │ ├── StandardDecrypter.java │ │ └── StandardEncrypter.java │ │ ├── exception │ │ ├── ZipExceptionConstants.java │ │ └── ZipException.java │ │ ├── model │ │ ├── CentralDirectory.java │ │ ├── ExtraDataRecord.java │ │ ├── DataDescriptor.java │ │ ├── DigitalSignature.java │ │ ├── ArchiveExtraDataRecord.java │ │ ├── Zip64EndCentralDirLocator.java │ │ ├── Zip64ExtendedInfo.java │ │ ├── AESExtraDataRecord.java │ │ ├── UnzipEngineParameters.java │ │ ├── UnzipParameters.java │ │ ├── EndCentralDirRecord.java │ │ ├── Zip64EndCentralDirRecord.java │ │ └── ZipModel.java │ │ ├── util │ │ ├── Zip4jConstants.java │ │ └── CRCUtil.java │ │ ├── unzip │ │ └── UnzipUtil.java │ │ └── progress │ │ └── ProgressMonitor.java └── build.gradle ├── simplefilesync ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── ditronic │ │ │ └── simplefilesync │ │ │ └── util │ │ │ ├── ResultCode.java │ │ │ ├── SSyncResult.java │ │ │ └── FilesUtil.java │ ├── release │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ └── debug │ │ └── res │ │ └── values │ │ └── strings.xml ├── build.gradle └── README.md ├── settings.gradle ├── dmitri-report-f15-16.pdf ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── BACKLOG.md ├── .github └── workflows │ └── ci.yml ├── CHANGELOG.md ├── gradle.properties ├── LICENSE ├── PRIVACY_POLICY.md ├── run_instrumentation_tests.sh ├── gradlew.bat ├── README.md └── .gitignore /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /zip4j/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /simplefilesync/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':simplefilesync', ':zip4j' 2 | -------------------------------------------------------------------------------- /app/src/androidTest/assets/invalid/emptyzip.zip: -------------------------------------------------------------------------------- 1 | PK -------------------------------------------------------------------------------- /zip4j/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /simplefilesync/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dmitri-report-f15-16.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/dmitri-report-f15-16.pdf -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/androidTest/assets/4notes.aeszip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/4notes.aeszip -------------------------------------------------------------------------------- /app/src/androidTest/assets/subdirs.aeszip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/subdirs.aeszip -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dev Secure Zip Notes 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/release/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Secure Zip Notes 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/androidTest/assets/compression/100a.txt: -------------------------------------------------------------------------------- 1 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -------------------------------------------------------------------------------- /app/src/androidTest/assets/singlenote.aeszip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/singlenote.aeszip -------------------------------------------------------------------------------- /app/src/androidTest/assets/twopasswords.ZIP: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/twopasswords.ZIP -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/androidTest/assets/invalid/emptynote.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/invalid/emptynote.7z -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/androidTest/assets/4passwords_subdirs.aeszip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/4passwords_subdirs.aeszip -------------------------------------------------------------------------------- /app/src/androidTest/assets/compression/100a_flat.aeszip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/compression/100a_flat.aeszip -------------------------------------------------------------------------------- /app/src/androidTest/assets/compression/100a_lzma.aeszip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/compression/100a_lzma.aeszip -------------------------------------------------------------------------------- /app/src/androidTest/assets/compression/100a_ppmd.aeszip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/compression/100a_ppmd.aeszip -------------------------------------------------------------------------------- /app/src/androidTest/assets/invalid/broken_zipcrypto.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/invalid/broken_zipcrypto.zip -------------------------------------------------------------------------------- /app/src/androidTest/assets/invalid/unencrypted_note.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/invalid/unencrypted_note.zip -------------------------------------------------------------------------------- /app/src/androidTest/assets/compression/100a_bzip2.aeszip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/compression/100a_bzip2.aeszip -------------------------------------------------------------------------------- /app/src/androidTest/assets/compression/100a_deflate.aeszip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/compression/100a_deflate.aeszip -------------------------------------------------------------------------------- /app/src/androidTest/assets/compression/100a_deflate64.aeszip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/compression/100a_deflate64.aeszip -------------------------------------------------------------------------------- /app/src/androidTest/assets/compression/100a_deflate_fast.aeszip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/compression/100a_deflate_fast.aeszip -------------------------------------------------------------------------------- /app/src/androidTest/assets/compression/100a_deflate_ultra.aeszip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fkirc/secure-zip-notes/HEAD/app/src/androidTest/assets/compression/100a_deflate_ultra.aeszip -------------------------------------------------------------------------------- /app/src/main/res/xml/provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #86DFEB 4 | #86DFEB 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Dec 13 19:23:51 CET 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip 7 | -------------------------------------------------------------------------------- /simplefilesync/src/release/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 62deec4uibwo7e2 5 | db-62deec4uibwo7e2 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_noteselect_longclick.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | -------------------------------------------------------------------------------- /simplefilesync/src/main/java/com/ditronic/simplefilesync/util/ResultCode.java: -------------------------------------------------------------------------------- 1 | package com.ditronic.simplefilesync.util; 2 | 3 | public enum ResultCode { 4 | UPLOAD_SUCCESS, 5 | DOWNLOAD_SUCCESS, 6 | REMOTE_EQUALS_LOCAL, 7 | FILES_NOT_EXIST_OR_EMPTY, 8 | NO_CREDENTIALS_FAILURE, 9 | CONNECTION_FAILURE, 10 | } 11 | -------------------------------------------------------------------------------- /simplefilesync/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | b7hsm39zkdqdgud 5 | db-b7hsm39zkdqdgud 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/common_toolbar.xml: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_add_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /BACKLOG.md: -------------------------------------------------------------------------------- 1 | This file contains a list of potential tasks for the continuous development of Secure Zip Notes. 2 | 3 | - Simple search functionality for open notes. 4 | 5 | - Option to use a pseudo keyboard for typing the master password, that is, a set of buttons that aims to protect users against keylogging malware. 6 | 7 | - Password autofill with an autofill framework starting from Android 8. 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_warning_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_arrow_forward_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/app/AppEnvironment.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.app 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | 6 | @SuppressLint("StaticFieldLeak") 7 | class AppEnvironment(val context: Context) { 8 | 9 | companion object { 10 | lateinit var current: AppEnvironment 11 | 12 | fun initialize(context: Context) { 13 | current = AppEnvironment(context = context) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/test/java/com/ditronic/securezipnotes/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_cloud_download_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/io/BaseInputStream.java: -------------------------------------------------------------------------------- 1 | package net.lingala.zip4j.io; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | import net.lingala.zip4j.unzip.UnzipEngine; 7 | 8 | public abstract class BaseInputStream extends InputStream { 9 | 10 | public int read() throws IOException { 11 | return 0; 12 | } 13 | 14 | public void seek(long pos) throws IOException { 15 | } 16 | 17 | public int available() throws IOException { 18 | return 0; 19 | } 20 | 21 | public UnzipEngine getUnzipEngine() { 22 | return null; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_noteedit.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /zip4j/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 29 5 | 6 | defaultConfig { 7 | minSdkVersion 19 8 | targetSdkVersion 29 9 | versionCode 1 10 | versionName "SNAPSHOT" 11 | } 12 | 13 | compileOptions { 14 | targetCompatibility = '1.8' 15 | sourceCompatibility = '1.8' 16 | } 17 | 18 | lintOptions { 19 | abortOnError false 20 | } 21 | } 22 | 23 | dependencies { 24 | implementation fileTree(dir: 'libs', include: ['*.jar']) 25 | 26 | implementation 'org.projectlombok:lombok:1.18.16' 27 | annotationProcessor 'org.projectlombok:lombok:1.18.16' 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | schedule: 7 | - cron: '0 12 15 * *' 8 | 9 | jobs: 10 | pre_job: 11 | runs-on: ubuntu-latest 12 | outputs: 13 | should_skip: ${{ steps.skip_check.outputs.should_skip }} 14 | steps: 15 | - id: skip_check 16 | uses: fkirc/skip-duplicate-actions@master 17 | with: 18 | paths_ignore: '["**/*.md"]' 19 | 20 | chain: 21 | needs: pre_job 22 | if: ${{ needs.pre_job.outputs.should_skip == 'false' }} 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: check 27 | run: bash ./gradlew check 28 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Secure Zip Notes 3 | 4 | ca-app-pub-3394747202744753~4306949160 5 | 6 | Create New Zip File 7 | Import Zip File 8 | Export Zip File 9 | Master Password 10 | Sync with Dropbox 11 | Sync with Google Drive 12 | 13 | Notes empty, nothing to export 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/app/ApplicationExt.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.app 2 | 3 | 4 | import android.app.Application 5 | import com.ditronic.securezipnotes.BuildConfig 6 | import com.ditronic.securezipnotes.logging.CrashlyticsTree 7 | import timber.log.Timber 8 | 9 | class ApplicationExt : Application() { 10 | 11 | override fun onCreate() { 12 | super.onCreate() 13 | AppEnvironment.initialize(context = this) 14 | 15 | initLogging() 16 | } 17 | 18 | private fun initLogging() { 19 | if (BuildConfig.DEBUG) { 20 | Timber.plant(Timber.DebugTree()) 21 | } else { 22 | Timber.plant(CrashlyticsTree()) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/util/OnThrottleClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.util 2 | 3 | import android.os.SystemClock 4 | import android.view.View 5 | 6 | abstract class OnThrottleClickListener : View.OnClickListener { 7 | 8 | private var lastClickTime: Long = 0 9 | protected abstract fun onThrottleClick(v: View) 10 | 11 | override fun onClick(v: View) { 12 | val clickTime = SystemClock.elapsedRealtime() 13 | if (clickTime - lastClickTime < THROTTLE_TIME) { 14 | return 15 | } 16 | lastClickTime = clickTime 17 | onThrottleClick(v) 18 | } 19 | 20 | companion object { 21 | 22 | private const val THROTTLE_TIME: Long = 400 // milliseconds 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_noteselect.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 15 | 16 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.0 2 | 3 | - Remove Google Drive sync to remove bugs and streamline maintenance. 4 | 5 | ## 1.1.4 6 | 7 | - Maintenance update (keeping up with Android updates and library updates) 8 | 9 | ## 1.1.3 10 | 11 | - Maintenance update 12 | 13 | ## 1.1.2 14 | 15 | - Maintenance update 16 | 17 | ## 1.1.1 18 | 19 | - Remove text formatting when pasting notes. From now on, everything is plain text. 20 | 21 | - More friendly error messages when parsing invalid or non-compliant ZIP files. 22 | 23 | ## 1.1.0 24 | 25 | - Uses fingerprints (or other biometrics) instead of PIN-Codes. 26 | This improves the usability while still retaining the security of hardware-protected keys. 27 | 28 | - Fixing a bug for Android 5.0. 29 | 30 | - No more hidden file extensions. 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/app/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.app 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.ditronic.securezipnotes.noteedit.NoteEditViewModel 6 | 7 | @Suppress("UNCHECKED_CAST") 8 | class ViewModelFactory(private val environment: AppEnvironment) : ViewModelProvider.Factory { 9 | 10 | override fun create(modelClass: Class): T { 11 | return when { 12 | modelClass.isAssignableFrom(NoteEditViewModel::class.java) -> NoteEditViewModel( 13 | appContext = environment.context 14 | ) as T 15 | else -> throw IllegalArgumentException("Unknown ViewModel - Maybe forgot to add in ViewModelFactory?") 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/noteedit/NoteEditViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.noteedit 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import androidx.lifecycle.ViewModel 6 | import com.ditronic.securezipnotes.zip.CryptoZip 7 | import net.lingala.zip4j.model.FileHeader 8 | 9 | @SuppressLint("StaticFieldLeak") 10 | class NoteEditViewModel(val appContext: Context) : ViewModel() { 11 | 12 | internal var secretContent: String? = null 13 | internal var editMode: Boolean = false 14 | 15 | lateinit var innerFileName: String 16 | 17 | internal val fileHeader: FileHeader 18 | get() = CryptoZip.instance(appContext).getFileHeader(innerFileName)!! 19 | 20 | internal val noteName: String 21 | get() = CryptoZip.getDisplayName(fileHeader) 22 | } 23 | -------------------------------------------------------------------------------- /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 | android.enableJetifier=true 10 | android.useAndroidX=true 11 | android.enableR8=true 12 | org.gradle.jvmargs=-Xmx1536m 13 | # When configured, Gradle will run in incubating parallel mode. 14 | # This option should only be used with decoupled projects. More details, visit 15 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 16 | # org.gradle.parallel=true 17 | -------------------------------------------------------------------------------- /simplefilesync/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 30 5 | 6 | 7 | defaultConfig { 8 | minSdkVersion 19 9 | targetSdkVersion 30 10 | versionCode 1 11 | versionName "1.0.0" 12 | } 13 | 14 | lintOptions { 15 | abortOnError true 16 | warning 'InvalidPackage' 17 | } 18 | } 19 | 20 | dependencies { 21 | implementation fileTree(dir: 'libs', include: ['*.jar']) 22 | 23 | implementation "com.jakewharton.timber:timber:4.7.1" 24 | implementation 'com.google.android.gms:play-services-auth:19.0.0' 25 | implementation ('com.google.http-client:google-http-client-gson:1.38.0') { 26 | exclude group: 'org.apache.httpcomponents' 27 | } 28 | 29 | implementation 'com.dropbox.core:dropbox-core-sdk:3.1.5' 30 | api 'com.squareup.okhttp3:okhttp:4.9.0' 31 | } 32 | -------------------------------------------------------------------------------- /app/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/io/ZipOutputStream.java: -------------------------------------------------------------------------------- 1 | package net.lingala.zip4j.io; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | 6 | import net.lingala.zip4j.model.ZipModel; 7 | 8 | public class ZipOutputStream extends DeflaterOutputStream { 9 | 10 | public ZipOutputStream(OutputStream outputStream) { 11 | this(outputStream, null); 12 | } 13 | 14 | public ZipOutputStream(OutputStream outputStream, ZipModel zipModel) { 15 | super(outputStream, zipModel); 16 | } 17 | 18 | public void write(int bval) throws IOException { 19 | byte[] b = new byte[1]; 20 | b[0] = (byte) bval; 21 | write(b, 0, 1); 22 | } 23 | 24 | public void write(byte[] b) throws IOException { 25 | write(b, 0, b.length); 26 | } 27 | 28 | public void write(byte[] b, int off, int len) throws IOException { 29 | crc.update(b, off, len); 30 | updateTotalBytesRead(len); 31 | super.write(b, off, len); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dropbox.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/noteedit/PlainEditText.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.noteedit 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.util.AttributeSet 6 | import androidx.appcompat.widget.AppCompatEditText 7 | 8 | class PlainEditText : AppCompatEditText { 9 | constructor(context: Context) : super(context) 10 | 11 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) 12 | 13 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) 14 | 15 | override fun onTextContextMenuItem(id: Int): Boolean { 16 | var newId = id 17 | if (id == android.R.id.paste) { 18 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 19 | newId = android.R.id.pasteAsPlainText 20 | } 21 | } 22 | return super.onTextContextMenuItem(newId) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/util/OnThrottleItemClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.util 2 | 3 | import android.os.SystemClock 4 | import android.view.View 5 | import android.widget.AdapterView 6 | 7 | abstract class OnThrottleItemClickListener : AdapterView.OnItemClickListener { 8 | 9 | private var lastClickTime: Long = 0 10 | protected abstract fun onThrottleItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) 11 | 12 | override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { 13 | val clickTime = SystemClock.elapsedRealtime() 14 | if (clickTime - lastClickTime < THROTTLE_TIME) { 15 | return 16 | } 17 | lastClickTime = clickTime 18 | onThrottleItemClick(parent, view, position, id) 19 | } 20 | 21 | companion object { 22 | 23 | private const val THROTTLE_TIME: Long = 500 // milliseconds 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/io/BaseOutputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.io; 18 | 19 | import java.io.IOException; 20 | import java.io.OutputStream; 21 | 22 | public abstract class BaseOutputStream extends OutputStream { 23 | 24 | public void write(int b) throws IOException { 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/crypto/IDecrypter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.crypto; 18 | 19 | import net.lingala.zip4j.exception.ZipException; 20 | 21 | public interface IDecrypter { 22 | 23 | public int decryptData(byte[] buff, int start, int len) throws ZipException; 24 | 25 | public int decryptData(byte[] buff) throws ZipException; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/crypto/IEncrypter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.crypto; 18 | 19 | import net.lingala.zip4j.exception.ZipException; 20 | 21 | public interface IEncrypter { 22 | 23 | public int encryptData(byte[] buff) throws ZipException; 24 | 25 | public int encryptData(byte[] buff, int start, int len) throws ZipException; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_drive.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/crypto/PBKDF2/PRF.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.crypto.PBKDF2; 18 | 19 | /* 20 | * Source referred from Matthias Gartner's PKCS#5 implementation - 21 | * see http://rtner.de/software/PBKDF2.html 22 | */ 23 | 24 | interface PRF 25 | { 26 | public void init(byte[] P); 27 | 28 | public byte[] doFinal(byte[] M); 29 | 30 | public int getHLen(); 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/dialogs/DeleteDialog.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.dialogs 2 | 3 | import android.app.Dialog 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AlertDialog 6 | 7 | abstract class DeleteDialogState(val message: String) { 8 | abstract fun onPositiveClick() 9 | abstract fun onNegativeClick() 10 | } 11 | 12 | class DeleteDialog: ShortLifeDialogFragment() { 13 | 14 | companion object { 15 | val TAG = FragmentTag("DeleteDialog") 16 | } 17 | 18 | override fun onCreateDialog(savedInstanceState: Bundle?, state: DeleteDialogState): Dialog { 19 | return AlertDialog.Builder(requireContext()) 20 | .setIcon(android.R.drawable.ic_dialog_alert) 21 | .setMessage(state.message) 22 | .setPositiveButton("OK") { _, _ -> state.onPositiveClick() } 23 | .setNegativeButton("Cancel") { _, _ -> state.onNegativeClick() } 24 | .create() 25 | } 26 | 27 | override fun getFragmentTag() = TAG 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/exception/ZipExceptionConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.exception; 18 | 19 | public interface ZipExceptionConstants { 20 | 21 | public static int inputZipParamIsNull = 0001; 22 | 23 | public static int constuctorFileNotFoundException = 0002; 24 | 25 | public static int randomAccessFileNull = 0003; 26 | 27 | public static int notZipFile = 0004; 28 | 29 | public static int WRONG_PASSWORD = 0005; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/logging/CrashlyticsTree.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.logging 2 | 3 | import android.util.Log 4 | import com.google.firebase.crashlytics.FirebaseCrashlytics 5 | import timber.log.Timber 6 | 7 | class CrashlyticsTree : Timber.Tree() { 8 | 9 | private val crashlytics by lazy { 10 | FirebaseCrashlytics.getInstance() 11 | } 12 | 13 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { 14 | if (priority == Log.ERROR || priority == Log.ASSERT) { 15 | logErrorWithoutCrash(message, t) 16 | } 17 | } 18 | 19 | private fun logErrorWithoutCrash(message: String, t: Throwable?) { 20 | if (t != null) { 21 | // If we have an exception, then the message is most likely redundant since it will also contain the stacktrace. 22 | crashlytics.recordException(t) 23 | } else { 24 | // If we do not have an exception, then we wrap the message inside an exception. 25 | crashlytics.recordException(IllegalStateException(message)) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /PRIVACY_POLICY.md: -------------------------------------------------------------------------------- 1 | # Secure Zip Notes Privacy Policy 2 | 3 | DiTronic Apps built Secure Zip Notes with privacy and security as the top priority. 4 | By default, Secure Zip Notes is a pure "offline app". 5 | The only exception is the optional sync with a cloud backend (e.g. Dropbox). 6 | Even if cloud sync is enabled, the cloud provider is still not able to decrypt the data since Secure Zip Notes encrypts all notes locally before uploading them. 7 | Secure Zip Notes uses the oauth2 protocol to access Dropbox. 8 | Secure Zip Notes only requests the minimal oauth2 scopes that are necessary for creating app-specific files. 9 | Therefore, Secure Zip Notes does not have the permission to access any cloud files that have not been generated by Secure Zip Notes. 10 | Secure Zip Notes is open-source. 11 | If you are suspicious about the implementation of Secure Zip Notes, then you can review the source code at https://github.com/fkirc/secure-zip-notes. 12 | 13 | # Children’s Privacy 14 | 15 | Children are free to use our app since it only stores text that is typed in by the user. 16 | It does not display any content that is retrieved from the Internet or other users. 17 | -------------------------------------------------------------------------------- /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 22 | 23 | # Since this app is open-source I do not want to care about proguard deobfuscation. 24 | -dontobfuscate 25 | 26 | # ---------------------------------------------------------- 27 | # This stuff is copied from the Dropbox Core SDK 28 | -dontwarn okio.** 29 | -dontwarn okhttp3.** 30 | -dontwarn com.squareup.okhttp.** 31 | -dontwarn com.google.appengine.** 32 | -dontwarn javax.servlet.** 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/util/BannerAds.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.util 2 | 3 | import android.app.Activity 4 | 5 | /*import com.google.android.gms.ads.AdRequest; 6 | import com.google.android.gms.ads.AdSize; 7 | import com.google.android.gms.ads.AdView; 8 | import com.google.android.gms.ads.MobileAds;*/ 9 | 10 | @Suppress("UNUSED_PARAMETER") 11 | object BannerAds { 12 | 13 | const val TEST_AD_UNIT_ID = "ca-app-pub-3940256099942544/6300978111" 14 | const val PRODUCTION_AD_UNIT_ID = "ca-app-pub-3394747202744753/2613883108" 15 | 16 | fun loadBottomAdsBanner(ac: Activity) { 17 | 18 | /*MobileAds.initialize(ac, ac.getResources().getString(R.string.admob_app_id)); 19 | final AdView mAdView = new AdView(ac); 20 | mAdView.setAdSize(AdSize.BANNER); 21 | if (BuildConfig.DEBUG) { 22 | mAdView.setAdUnitId(TEST_AD_UNIT_ID); 23 | } else { 24 | mAdView.setAdUnitId(PRODUCTION_AD_UNIT_ID); 25 | } 26 | // We need to create the AdView dynamically to set the ad unit id dynamically 27 | final FrameLayout frameLayout = ac.findViewById(R.id.adView); 28 | frameLayout.addView(mAdView); 29 | AdRequest adRequest = new AdRequest.Builder().build(); 30 | mAdView.loadAd(adRequest);*/ 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/model/CentralDirectory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.model; 18 | 19 | import java.util.ArrayList; 20 | 21 | public class CentralDirectory { 22 | 23 | private ArrayList fileHeaders; 24 | 25 | private DigitalSignature digitalSignature; 26 | 27 | public ArrayList getFileHeaders() { 28 | return fileHeaders; 29 | } 30 | 31 | public void setFileHeaders(ArrayList fileHeaders) { 32 | this.fileHeaders = fileHeaders; 33 | } 34 | 35 | public DigitalSignature getDigitalSignature() { 36 | return digitalSignature; 37 | } 38 | 39 | public void setDigitalSignature(DigitalSignature digitalSignature) { 40 | this.digitalSignature = digitalSignature; 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/model/ExtraDataRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.model; 18 | 19 | public class ExtraDataRecord { 20 | 21 | private long header; 22 | 23 | private int sizeOfData; 24 | 25 | private byte[] data; 26 | 27 | public long getHeader() { 28 | return header; 29 | } 30 | 31 | public void setHeader(long header) { 32 | this.header = header; 33 | } 34 | 35 | public int getSizeOfData() { 36 | return sizeOfData; 37 | } 38 | 39 | public void setSizeOfData(int sizeOfData) { 40 | this.sizeOfData = sizeOfData; 41 | } 42 | 43 | public byte[] getData() { 44 | return data; 45 | } 46 | 47 | public void setData(byte[] data) { 48 | this.data = data; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /simplefilesync/src/main/java/com/ditronic/simplefilesync/util/SSyncResult.java: -------------------------------------------------------------------------------- 1 | package com.ditronic.simplefilesync.util; 2 | 3 | import java.io.File; 4 | 5 | public class SSyncResult { 6 | 7 | private boolean syncTriggeredByUser; 8 | private File tmpDownloadFile; 9 | private ResultCode resultCode; 10 | 11 | public SSyncResult(final ResultCode resultCode) { 12 | this.resultCode = resultCode; 13 | } 14 | 15 | public boolean success() { 16 | if (resultCode == ResultCode.UPLOAD_SUCCESS 17 | || resultCode == ResultCode.DOWNLOAD_SUCCESS 18 | || resultCode == ResultCode.REMOTE_EQUALS_LOCAL 19 | || resultCode == ResultCode.FILES_NOT_EXIST_OR_EMPTY 20 | ) { 21 | return true; 22 | } 23 | return false; 24 | } 25 | 26 | public ResultCode getResultCode() { 27 | return resultCode; 28 | } 29 | 30 | public boolean isSyncTriggeredByUser() { 31 | return syncTriggeredByUser; 32 | } 33 | 34 | public void setSyncTriggeredByUser(boolean syncTriggeredByUser) { 35 | this.syncTriggeredByUser = syncTriggeredByUser; 36 | } 37 | 38 | public File getTmpDownloadFile() { 39 | return tmpDownloadFile; 40 | } 41 | 42 | public void setTmpDownloadFile(File tmpDownloadFile) { 43 | this.tmpDownloadFile = tmpDownloadFile; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/ditronic/securezipnotes/contrib/NonFatalAbortTree.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.contrib 2 | 3 | import android.util.Log 4 | import androidx.test.espresso.Espresso 5 | import org.junit.Assert 6 | import timber.log.Timber 7 | 8 | /** 9 | * During UI tests, we want to fail the test immediately if any "non-fatal error" happens. 10 | * The purpose of non-fatal errors is to inform as about production failures without crashing the entire app. 11 | */ 12 | object NonFatalAbortTree: Timber.Tree() { 13 | override fun log(priority: Int, tag: String?, message: String, throwable: Throwable?) { 14 | if (priority == Log.ERROR || priority == Log.ASSERT) { 15 | abortTest(message=message, throwable = throwable) 16 | } 17 | } 18 | 19 | private fun abortTest(message: String, throwable: Throwable?) { 20 | if (disabled) { 21 | return 22 | } 23 | Assert.fail(message) 24 | throw IllegalStateException(message, throwable) 25 | } 26 | 27 | fun plantInTimber() { 28 | Timber.plant(this) 29 | } 30 | 31 | private var disabled = false 32 | 33 | fun runWithoutAborts(waitIdle: Boolean = true, runnable: () -> Unit) { 34 | disabled = true 35 | runnable() 36 | if (waitIdle) { 37 | Espresso.onIdle() 38 | } 39 | disabled = false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/model/DataDescriptor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.model; 18 | 19 | public class DataDescriptor { 20 | 21 | private String crc32; 22 | 23 | private int compressedSize; 24 | 25 | private int uncompressedSize; 26 | 27 | public String getCrc32() { 28 | return crc32; 29 | } 30 | 31 | public void setCrc32(String crc32) { 32 | this.crc32 = crc32; 33 | } 34 | 35 | public int getCompressedSize() { 36 | return compressedSize; 37 | } 38 | 39 | public void setCompressedSize(int compressedSize) { 40 | this.compressedSize = compressedSize; 41 | } 42 | 43 | public int getUncompressedSize() { 44 | return uncompressedSize; 45 | } 46 | 47 | public void setUncompressedSize(int uncompressedSize) { 48 | this.uncompressedSize = uncompressedSize; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/model/DigitalSignature.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.model; 18 | 19 | public class DigitalSignature { 20 | 21 | private int headerSignature; 22 | 23 | private int sizeOfData; 24 | 25 | private String signatureData; 26 | 27 | public int getHeaderSignature() { 28 | return headerSignature; 29 | } 30 | 31 | public void setHeaderSignature(int headerSignature) { 32 | this.headerSignature = headerSignature; 33 | } 34 | 35 | public int getSizeOfData() { 36 | return sizeOfData; 37 | } 38 | 39 | public void setSizeOfData(int sizeOfData) { 40 | this.sizeOfData = sizeOfData; 41 | } 42 | 43 | public String getSignatureData() { 44 | return signatureData; 45 | } 46 | 47 | public void setSignatureData(String signatureData) { 48 | this.signatureData = signatureData; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/model/ArchiveExtraDataRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.model; 18 | 19 | public class ArchiveExtraDataRecord { 20 | 21 | private int signature; 22 | 23 | private int extraFieldLength; 24 | 25 | private String extraFieldData; 26 | 27 | public int getSignature() { 28 | return signature; 29 | } 30 | 31 | public void setSignature(int signature) { 32 | this.signature = signature; 33 | } 34 | 35 | public int getExtraFieldLength() { 36 | return extraFieldLength; 37 | } 38 | 39 | public void setExtraFieldLength(int extraFieldLength) { 40 | this.extraFieldLength = extraFieldLength; 41 | } 42 | 43 | public String getExtraFieldData() { 44 | return extraFieldData; 45 | } 46 | 47 | public void setExtraFieldData(String extraFieldData) { 48 | this.extraFieldData = extraFieldData; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/zip/ZipUtil.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.zip 2 | 3 | import android.content.Context 4 | import com.ditronic.securezipnotes.util.Boast 5 | import net.lingala.zip4j.io.ZipInputStream 6 | import java.io.IOException 7 | import java.io.InputStreamReader 8 | import java.nio.charset.StandardCharsets 9 | 10 | 11 | @Throws(IOException::class) 12 | fun inputStreamToString(`is`: ZipInputStream): String { 13 | val ir = InputStreamReader(`is`, StandardCharsets.UTF_8) 14 | val sb = StringBuilder() 15 | val buf = CharArray(1024) 16 | while (true) { 17 | val n : Int = ir.read(buf) 18 | if (n != -1) { 19 | sb.append(buf, 0, n) 20 | } else { 21 | break 22 | } 23 | } 24 | return sb.toString() 25 | } 26 | 27 | 28 | fun validateEntryNameToast(newEntryName: String, cx : Context) : Boolean { 29 | if (newEntryName.isEmpty()) { 30 | Boast.makeText(cx, "Empty file names are not allowed").show() 31 | return false 32 | } 33 | if (isIllegalEntryName(newEntryName)) { 34 | Boast.makeText(cx, newEntryName + " is an invalid entry name").show() 35 | return false 36 | } 37 | return true 38 | } 39 | 40 | 41 | private fun isIllegalEntryName(entryName: String) : Boolean { 42 | if (entryName.startsWith("/")) 43 | return true 44 | if (entryName.startsWith("\\")) 45 | return true 46 | if (entryName.endsWith("/")) 47 | return true 48 | if (entryName.endsWith("\\")) 49 | return true 50 | return false 51 | } 52 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/ditronic/securezipnotes/robotpattern/Preconditions.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.robotpattern 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import com.ditronic.securezipnotes.contrib.NonFatalAbortTree 5 | import com.ditronic.securezipnotes.noteselect.MainActivity 6 | import com.ditronic.securezipnotes.testutils.launchActivity 7 | import com.ditronic.securezipnotes.zip.CryptoZip 8 | import java.io.FileOutputStream 9 | 10 | private fun commonPreconditions() { 11 | NonFatalAbortTree.plantInTimber() 12 | launchActivity(MainActivity::class.java) 13 | } 14 | 15 | fun precondition_cleanStart() { 16 | commonPreconditions() 17 | } 18 | 19 | fun precondition_singleNote() { 20 | precondition_loadAsset("singlenote.aeszip") 21 | main_assertListState(listOf("Note 1")) 22 | } 23 | 24 | fun precondition_fourNotes() { 25 | precondition_loadAsset("4notes.aeszip") 26 | main_assertListState(listOf("Note 1", "Note 2", "Note 3", "Note 4").reversed()) 27 | } 28 | 29 | fun precondition_loadAsset(assetPath: String) { 30 | loadAsset(assetPath) 31 | commonPreconditions() 32 | } 33 | 34 | private fun loadAsset(assetPath: String) { 35 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 36 | val testContext = InstrumentationRegistry.getInstrumentation().context 37 | 38 | val sourceStream = testContext.assets.open(assetPath) 39 | val targetStream = FileOutputStream(CryptoZip.getMainFilePath(appContext)) 40 | sourceStream.copyTo(targetStream) 41 | sourceStream.close() 42 | targetStream.close() 43 | } 44 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/exception/ZipException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.exception; 18 | 19 | public class ZipException extends Exception { 20 | 21 | private static final long serialVersionUID = 1L; 22 | 23 | private int code = -1; 24 | 25 | public ZipException() { 26 | } 27 | 28 | public ZipException(String msg) { 29 | super(msg); 30 | } 31 | 32 | public ZipException(String message, Throwable cause) { 33 | super(message, cause); 34 | } 35 | 36 | public ZipException(String msg, int code) { 37 | super(msg); 38 | this.code = code; 39 | } 40 | 41 | public ZipException(String message, Throwable cause, int code) { 42 | super(message, cause); 43 | this.code = code; 44 | } 45 | 46 | public ZipException(Throwable cause) { 47 | super(cause); 48 | } 49 | 50 | public ZipException(Throwable cause, int code) { 51 | super(cause); 52 | this.code = code; 53 | } 54 | 55 | public int getCode() { 56 | return code; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/res/layout/cardview.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 22 | 23 | 27 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_note_edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 21 | 22 | 23 | 27 | 28 | 29 | 36 | 37 | 38 | 42 | 43 | -------------------------------------------------------------------------------- /run_instrumentation_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | set -x 4 | 5 | # This is just an experimental script to capture some commands. 6 | # Real testing is performed via gradle tasks or Android Studio. 7 | 8 | # Debug 9 | TARGET_APP=com.ditronic.securezipnotes.dev 10 | # Release 11 | #TARGET_APP=com.ditronic.securezipnotes 12 | 13 | WHICH_TESTS='com.ditronic.securezipnotes.tests.ITest#deleteLastNote' 14 | ORCHESTRATOR_VERSION='1.2.0' 15 | 16 | # Install app 17 | adb push app/build/outputs/apk/debug/app-debug.apk /data/local/tmp/$TARGET_APP 18 | adb shell pm install --full -t -r "/data/local/tmp/$TARGET_APP" 19 | 20 | # Install androidTest app 21 | adb push app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk /data/local/tmp/$TARGET_APP.test 22 | adb shell pm install --full -t -r "/data/local/tmp/$TARGET_APP.test" 23 | 24 | # Install orchestrator app 25 | adb push ~/.gradle/caches/modules-2/files-2.1/androidx.test/orchestrator/$ORCHESTRATOR_VERSION/e3edf4e08d2d3db127c1b56bbe405b90b64daa5/orchestrator-$ORCHESTRATOR_VERSION.apk /data/local/tmp/androidx.test.orchestrator 26 | adb shell pm install --full -t -r "/data/local/tmp/androidx.test.orchestrator" 27 | 28 | # Install test services apk 29 | adb push ~/.gradle/caches/modules-2/files-2.1/androidx.test.services/test-services/$ORCHESTRATOR_VERSION/58c9ac48a725a1a0437f4f6be352a42f60ed5a7d/test-services-$ORCHESTRATOR_VERSION.apk /data/local/tmp/androidx.test.services 30 | adb shell pm install --full -t -r "/data/local/tmp/androidx.test.services" 31 | 32 | # Run the tests 33 | adb shell 'CLASSPATH=$(pm path androidx.test.services)' app_process / androidx.test.services.shellexecutor.ShellMain am instrument -r -w -e targetInstrumentation $TARGET_APP.test/androidx.test.runner.AndroidJUnitRunner -e debug false -e class $WHICH_TESTS -e clearPackageData true androidx.test.orchestrator/androidx.test.orchestrator.AndroidTestOrchestrator 34 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/crypto/PBKDF2/PBKDF2HexFormatter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.crypto.PBKDF2; 18 | 19 | /* 20 | * Source referred from Matthias Gartner's PKCS#5 implementation - 21 | * see http://rtner.de/software/PBKDF2.html 22 | */ 23 | 24 | class PBKDF2HexFormatter 25 | { 26 | public boolean fromString(PBKDF2Parameters p, String s) 27 | { 28 | if (p == null || s == null) 29 | { 30 | return true; 31 | } 32 | 33 | String[] p123 = s.split(":"); 34 | if (p123 == null || p123.length != 3) 35 | { 36 | return true; 37 | } 38 | 39 | byte salt[] = BinTools.hex2bin(p123[0]); 40 | int iterationCount = Integer.parseInt(p123[1]); 41 | byte bDK[] = BinTools.hex2bin(p123[2]); 42 | 43 | p.setSalt(salt); 44 | p.setIterationCount(iterationCount); 45 | p.setDerivedKey(bDK); 46 | return false; 47 | } 48 | 49 | public String toString(PBKDF2Parameters p) 50 | { 51 | String s = BinTools.bin2hex(p.getSalt()) + ":" 52 | + String.valueOf(p.getIterationCount()) + ":" 53 | + BinTools.bin2hex(p.getDerivedKey()); 54 | return s; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/ditronic/securezipnotes/tests/MultiPasswordTests.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.tests 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.filters.LargeTest 5 | import com.ditronic.securezipnotes.robotpattern.TESTPASSWORD 6 | import com.ditronic.securezipnotes.robotpattern.main_assertListState 7 | import com.ditronic.securezipnotes.robotpattern.main_clickAssertCloseNote 8 | import com.ditronic.securezipnotes.robotpattern.precondition_loadAsset 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | 12 | 13 | @RunWith(AndroidJUnit4::class) 14 | @LargeTest 15 | class MultiPasswordTests { 16 | 17 | @Test 18 | fun twoPasswords() { 19 | precondition_loadAsset("twopasswords.ZIP") 20 | main_assertListState(entries = listOf("pw2_entry", "testpassword_entry")) 21 | 22 | main_clickAssertCloseNote(noteName = "testpassword_entry", secretContent = "My secret note", password = TESTPASSWORD) 23 | main_clickAssertCloseNote(noteName = "pw2_entry", secretContent = "secret", password = "pw2") 24 | main_clickAssertCloseNote(noteName = "testpassword_entry", secretContent = "My secret note", password = TESTPASSWORD) 25 | } 26 | 27 | @Test 28 | fun test4Passwords() { 29 | precondition_loadAsset("4passwords_subdirs.aeszip") 30 | main_assertListState(entries = listOf("pw4_entry", "pw3_entry/dir/dir/pw2", "pw2_entry", "pw1_entry/dir/pw1")) 31 | 32 | main_clickAssertCloseNote(noteName = "pw4_entry", secretContent = "pw4_secret", password = "pw4") 33 | main_clickAssertCloseNote(noteName = "pw3_entry/dir/dir/pw2", secretContent = "pw3_secret", password = "pw3") 34 | main_clickAssertCloseNote(noteName = "pw2_entry", secretContent = "pw2_secret", password = "pw2") 35 | main_clickAssertCloseNote(noteName = "pw2_entry", secretContent = "pw2_secret") 36 | main_clickAssertCloseNote(noteName = "pw1_entry/dir/pw1", secretContent = "pw1_secret", password = "pw1") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/model/Zip64EndCentralDirLocator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.model; 18 | 19 | public class Zip64EndCentralDirLocator { 20 | 21 | private long signature; 22 | 23 | private int noOfDiskStartOfZip64EndOfCentralDirRec; 24 | 25 | private long offsetZip64EndOfCentralDirRec; 26 | 27 | private int totNumberOfDiscs; 28 | 29 | public long getSignature() { 30 | return signature; 31 | } 32 | 33 | public void setSignature(long signature) { 34 | this.signature = signature; 35 | } 36 | 37 | public int getNoOfDiskStartOfZip64EndOfCentralDirRec() { 38 | return noOfDiskStartOfZip64EndOfCentralDirRec; 39 | } 40 | 41 | public void setNoOfDiskStartOfZip64EndOfCentralDirRec( 42 | int noOfDiskStartOfZip64EndOfCentralDirRec) { 43 | this.noOfDiskStartOfZip64EndOfCentralDirRec = noOfDiskStartOfZip64EndOfCentralDirRec; 44 | } 45 | 46 | public long getOffsetZip64EndOfCentralDirRec() { 47 | return offsetZip64EndOfCentralDirRec; 48 | } 49 | 50 | public void setOffsetZip64EndOfCentralDirRec(long offsetZip64EndOfCentralDirRec) { 51 | this.offsetZip64EndOfCentralDirRec = offsetZip64EndOfCentralDirRec; 52 | } 53 | 54 | public int getTotNumberOfDiscs() { 55 | return totNumberOfDiscs; 56 | } 57 | 58 | public void setTotNumberOfDiscs(int totNumberOfDiscs) { 59 | this.totNumberOfDiscs = totNumberOfDiscs; 60 | } 61 | 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "680419627907", 4 | "firebase_url": "https://zip-notes.firebaseio.com", 5 | "project_id": "secure-zip-notes", 6 | "storage_bucket": "secure-zip-notes.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:680419627907:android:e452ae4cd6765d4a8a5cb6", 12 | "android_client_info": { 13 | "package_name": "com.ditronic.securezipnotes" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "680419627907-0tst6ljm7i300pcbfjh0cfledohd24ld.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "AIzaSyB7HTjR6I997G8bYktR_Xv791ALUTRROyE" 25 | } 26 | ], 27 | "services": { 28 | "appinvite_service": { 29 | "other_platform_oauth_client": [ 30 | { 31 | "client_id": "680419627907-0tst6ljm7i300pcbfjh0cfledohd24ld.apps.googleusercontent.com", 32 | "client_type": 3 33 | } 34 | ] 35 | } 36 | } 37 | }, 38 | { 39 | "client_info": { 40 | "mobilesdk_app_id": "1:680419627907:android:9e6bbe18bfc6b03f8a5cb6", 41 | "android_client_info": { 42 | "package_name": "com.ditronic.securezipnotes.dev" 43 | } 44 | }, 45 | "oauth_client": [ 46 | { 47 | "client_id": "680419627907-0tst6ljm7i300pcbfjh0cfledohd24ld.apps.googleusercontent.com", 48 | "client_type": 3 49 | } 50 | ], 51 | "api_key": [ 52 | { 53 | "current_key": "AIzaSyB7HTjR6I997G8bYktR_Xv791ALUTRROyE" 54 | } 55 | ], 56 | "services": { 57 | "appinvite_service": { 58 | "other_platform_oauth_client": [ 59 | { 60 | "client_id": "680419627907-0tst6ljm7i300pcbfjh0cfledohd24ld.apps.googleusercontent.com", 61 | "client_type": 3 62 | } 63 | ] 64 | } 65 | } 66 | } 67 | ], 68 | "configuration_version": "1" 69 | } -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/crypto/engine/ZipCryptoEngine.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.crypto.engine; 18 | 19 | public class ZipCryptoEngine { 20 | 21 | private final int keys[] = new int[3]; 22 | private static final int[] CRC_TABLE = new int[256]; 23 | 24 | static { 25 | for (int i = 0; i < 256; i++) { 26 | int r = i; 27 | for (int j = 0; j < 8; j++) { 28 | if ((r & 1) == 1) { 29 | r = (r >>> 1) ^ 0xedb88320; 30 | } else { 31 | r >>>= 1; 32 | } 33 | } 34 | CRC_TABLE[i] = r; 35 | } 36 | } 37 | 38 | public ZipCryptoEngine() { 39 | } 40 | 41 | public void initKeys(char[] password) { 42 | keys[0] = 305419896; 43 | keys[1] = 591751049; 44 | keys[2] = 878082192; 45 | for (int i = 0; i < password.length; i++) { 46 | updateKeys((byte) (password[i] & 0xff)); 47 | } 48 | } 49 | 50 | public void updateKeys(byte charAt) { 51 | keys[0] = crc32(keys[0], charAt); 52 | keys[1] += keys[0] & 0xff; 53 | keys[1] = keys[1] * 134775813 + 1; 54 | keys[2] = crc32(keys[2], (byte) (keys[1] >> 24)); 55 | } 56 | 57 | private int crc32(int oldCrc, byte charAt) { 58 | return ((oldCrc >>> 8) ^ CRC_TABLE[(oldCrc ^ charAt) & 0xff]); 59 | } 60 | 61 | public byte decryptByte() { 62 | int temp = keys[2] | 2; 63 | return (byte) ((temp * (temp ^ 1)) >>> 8); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/dialogs/ShortLifeDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.dialogs 2 | 3 | import android.app.Dialog 4 | import android.os.Bundle 5 | import androidx.annotation.CallSuper 6 | import androidx.fragment.app.DialogFragment 7 | import androidx.fragment.app.FragmentActivity 8 | import timber.log.Timber 9 | 10 | fun DialogFragment.dismissCrashSafe() { 11 | try { 12 | dismiss() 13 | } catch (exception: Exception) { 14 | Timber.e(exception) 15 | } 16 | } 17 | 18 | data class FragmentTag(val value: String) 19 | 20 | /** 21 | * Generic dialog that can hold an arbitrary state and gets dismissed upon activity re-creation. 22 | */ 23 | abstract class ShortLifeDialogFragment: DialogFragment() { 24 | 25 | abstract fun getFragmentTag(): FragmentTag 26 | 27 | abstract fun onCreateDialog(savedInstanceState: Bundle?, state: EphemeralState): Dialog 28 | 29 | private var ephemeralState: EphemeralState? = null 30 | 31 | fun show(activity: FragmentActivity, state: EphemeralState) { 32 | this.ephemeralState = state 33 | dismissIfActive(activity = activity) 34 | show(activity.supportFragmentManager, getFragmentTag().value) 35 | } 36 | 37 | @CallSuper 38 | override fun onResume() { 39 | super.onResume() 40 | fetchStateOrDie() 41 | } 42 | 43 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 44 | val state = fetchStateOrDie() 45 | return if (state != null) { 46 | onCreateDialog(savedInstanceState, state) 47 | } else { 48 | androidx.appcompat.app.AlertDialog.Builder(requireContext()).create() 49 | } 50 | } 51 | 52 | protected fun fetchStateOrDie(): EphemeralState? { 53 | if (ephemeralState == null) { 54 | dismissCrashSafe() 55 | } 56 | return ephemeralState 57 | } 58 | 59 | private fun dismissIfActive(activity: FragmentActivity) { 60 | val fragmentManager = activity.supportFragmentManager 61 | val oldDialog = fragmentManager.findFragmentByTag(getFragmentTag().value) as? DialogFragment 62 | oldDialog?.dismissCrashSafe() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/noteselect/NoteSelectAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.noteselect 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.BaseAdapter 8 | import android.widget.TextView 9 | 10 | import java.text.SimpleDateFormat 11 | import java.util.Date 12 | import java.util.Locale 13 | 14 | import com.ditronic.securezipnotes.zip.CryptoZip 15 | import com.ditronic.securezipnotes.R 16 | 17 | import net.lingala.zip4j.model.FileHeader 18 | import net.lingala.zip4j.util.Zip4jUtil 19 | 20 | class NoteSelectAdapter(private val cx: Context) : BaseAdapter() { 21 | 22 | override fun getCount(): Int { 23 | return CryptoZip.instance(cx).numFileHeaders 24 | } 25 | 26 | override fun getItem(position: Int): Any { 27 | return CryptoZip.instance(cx).fileHeadersFast!![count - position - 1] 28 | } 29 | 30 | override fun getItemId(position: Int): Long { 31 | return position.toLong() 32 | } 33 | 34 | override fun getView(position: Int, convertViewF: View?, parent: ViewGroup): View { 35 | var convertView = convertViewF 36 | 37 | val fileHeader = getItem(position) as FileHeader 38 | 39 | if (convertView == null) { 40 | convertView = LayoutInflater.from(cx).inflate(R.layout.cardview, parent, false) 41 | } 42 | 43 | val tx = convertView!!.findViewById(R.id.txt_cardview) 44 | tx.text = CryptoZip.getDisplayName(fileHeader) 45 | 46 | val tx2 = convertView.findViewById(R.id.txt_cardview_2) 47 | val lastModZip = fileHeader.lastModFileTime 48 | val epochMillis = Date(Zip4jUtil.dosToJavaTme(lastModZip)) 49 | val lastModStr = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(epochMillis) 50 | tx2.text = lastModStr 51 | 52 | val tx3 = convertView.findViewById(R.id.txt_cardview_3) 53 | tx3.text = "Size: " + fileHeader.uncompressedSize 54 | 55 | return convertView 56 | } 57 | 58 | companion object { 59 | 60 | private val TAG = NoteSelectAdapter::class.java.simpleName 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/model/Zip64ExtendedInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.model; 18 | 19 | public class Zip64ExtendedInfo { 20 | 21 | private int header; 22 | 23 | private int size; 24 | 25 | private long compressedSize; 26 | 27 | private long unCompressedSize; 28 | 29 | private long offsetLocalHeader; 30 | 31 | private int diskNumberStart; 32 | 33 | public Zip64ExtendedInfo() { 34 | compressedSize = -1; 35 | unCompressedSize = -1; 36 | offsetLocalHeader = -1; 37 | diskNumberStart = -1; 38 | } 39 | 40 | public int getHeader() { 41 | return header; 42 | } 43 | 44 | public void setHeader(int header) { 45 | this.header = header; 46 | } 47 | 48 | public int getSize() { 49 | return size; 50 | } 51 | 52 | public void setSize(int size) { 53 | this.size = size; 54 | } 55 | 56 | public long getCompressedSize() { 57 | return compressedSize; 58 | } 59 | 60 | public void setCompressedSize(long compressedSize) { 61 | this.compressedSize = compressedSize; 62 | } 63 | 64 | public long getUnCompressedSize() { 65 | return unCompressedSize; 66 | } 67 | 68 | public void setUnCompressedSize(long unCompressedSize) { 69 | this.unCompressedSize = unCompressedSize; 70 | } 71 | 72 | public long getOffsetLocalHeader() { 73 | return offsetLocalHeader; 74 | } 75 | 76 | public void setOffsetLocalHeader(long offsetLocalHeader) { 77 | this.offsetLocalHeader = offsetLocalHeader; 78 | } 79 | 80 | public int getDiskNumberStart() { 81 | return diskNumberStart; 82 | } 83 | 84 | public void setDiskNumberStart(int diskNumberStart) { 85 | this.diskNumberStart = diskNumberStart; 86 | } 87 | 88 | 89 | 90 | } 91 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/model/AESExtraDataRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.model; 18 | 19 | public class AESExtraDataRecord { 20 | 21 | private long signature; 22 | private int dataSize; 23 | private int versionNumber; 24 | private String vendorID; 25 | private int aesStrength; 26 | private int compressionMethod; 27 | 28 | public AESExtraDataRecord() { 29 | signature = -1; 30 | dataSize = -1; 31 | versionNumber = -1; 32 | vendorID = null; 33 | aesStrength = -1; 34 | compressionMethod = -1; 35 | } 36 | 37 | 38 | public long getSignature() { 39 | return signature; 40 | } 41 | 42 | 43 | public void setSignature(long signature) { 44 | this.signature = signature; 45 | } 46 | 47 | 48 | public int getDataSize() { 49 | return dataSize; 50 | } 51 | 52 | 53 | public void setDataSize(int dataSize) { 54 | this.dataSize = dataSize; 55 | } 56 | 57 | 58 | public int getVersionNumber() { 59 | return versionNumber; 60 | } 61 | 62 | 63 | public void setVersionNumber(int versionNumber) { 64 | this.versionNumber = versionNumber; 65 | } 66 | 67 | 68 | public String getVendorID() { 69 | return vendorID; 70 | } 71 | 72 | 73 | public void setVendorID(String vendorID) { 74 | this.vendorID = vendorID; 75 | } 76 | 77 | 78 | public int getAesStrength() { 79 | return aesStrength; 80 | } 81 | 82 | 83 | public void setAesStrength(int aesStrength) { 84 | this.aesStrength = aesStrength; 85 | } 86 | 87 | 88 | public int getCompressionMethod() { 89 | return compressionMethod; 90 | } 91 | 92 | 93 | public void setCompressionMethod(int compressionMethod) { 94 | this.compressionMethod = compressionMethod; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/dialogs/RenameFileDialog.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.dialogs 2 | 3 | import android.app.Dialog 4 | import android.os.Bundle 5 | import android.text.InputType 6 | import android.view.WindowManager 7 | import android.widget.EditText 8 | import androidx.appcompat.app.AlertDialog 9 | import com.ditronic.securezipnotes.password.PwResult 10 | import com.ditronic.securezipnotes.zip.CryptoZip 11 | import net.lingala.zip4j.model.FileHeader 12 | 13 | abstract class RenameFileDialogState(val pwResult: PwResult.Success, 14 | val fileHeader: FileHeader) { 15 | abstract fun onRenameReturned() 16 | } 17 | 18 | class RenameFileDialog: ShortLifeDialogFragment() { 19 | 20 | companion object { 21 | val TAG = FragmentTag("RenameFileDialog") 22 | } 23 | 24 | lateinit var editText: EditText 25 | 26 | override fun onCreateDialog(savedInstanceState: Bundle?, state: RenameFileDialogState): Dialog { 27 | editText = EditText(requireContext()) 28 | 29 | val builder = AlertDialog.Builder(requireContext()) 30 | builder.setTitle("Rename " + CryptoZip.getDisplayName(state.fileHeader)) 31 | editText.inputType = InputType.TYPE_CLASS_TEXT 32 | editText.setText(CryptoZip.getDisplayName(state.fileHeader)) 33 | builder.setView(editText) 34 | builder.setPositiveButton(android.R.string.ok) { _, _ -> 35 | onRenameConfirmed() 36 | } 37 | builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() } 38 | val dialog = builder.create() 39 | val window = dialog.window 40 | window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) 41 | editText.requestFocus() 42 | return dialog 43 | } 44 | 45 | override fun getFragmentTag() = TAG 46 | 47 | private fun onRenameConfirmed() { 48 | val state = fetchStateOrDie() ?: return 49 | 50 | val newName = editText.text.toString() 51 | CryptoZip.instance(requireContext()).renameFile( 52 | pw = state.pwResult.password, 53 | fileHeader = state.fileHeader, 54 | newEntryName = newName, 55 | cx = requireContext()) 56 | 57 | state.onRenameReturned() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /simplefilesync/src/main/java/com/ditronic/simplefilesync/util/FilesUtil.java: -------------------------------------------------------------------------------- 1 | package com.ditronic.simplefilesync.util; 2 | 3 | import com.google.android.gms.common.util.Hex; 4 | 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.FileOutputStream; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.OutputStream; 11 | import java.security.MessageDigest; 12 | 13 | public class FilesUtil { 14 | 15 | public static File streamToTmpFile (final InputStream is) { 16 | final OutputStream os; 17 | final File tmpFile; 18 | try { 19 | tmpFile = File.createTempFile("stream2file", ".tmp"); 20 | tmpFile.deleteOnExit(); 21 | os = new FileOutputStream(tmpFile); 22 | byte[] buffer = new byte[1024]; 23 | int length; 24 | while ((length = is.read(buffer)) > 0) { 25 | os.write(buffer, 0, length); 26 | } 27 | is.close(); 28 | os.close(); 29 | } catch (IOException e) { 30 | throw new RuntimeException(e); 31 | } 32 | return tmpFile; 33 | } 34 | 35 | 36 | public static void copyFile(final File source, final File dest) throws IOException { 37 | InputStream is = null; 38 | OutputStream os = null; 39 | try { 40 | is = new FileInputStream(source); 41 | os = new FileOutputStream(dest); 42 | byte[] buffer = new byte[1024]; 43 | int length; 44 | while ((length = is.read(buffer)) > 0) { 45 | os.write(buffer, 0, length); 46 | } 47 | } finally { 48 | is.close(); 49 | os.close(); 50 | } 51 | } 52 | 53 | 54 | public static String hashFromFile(final java.io.File file, final MessageDigest hasher) { 55 | final byte[] buf = new byte[1024]; 56 | try { 57 | final InputStream in = new FileInputStream(file); 58 | while (true) { 59 | int n = in.read(buf); 60 | if (n < 0) break; // EOF 61 | hasher.update(buf, 0, n); 62 | } 63 | in.close(); 64 | return Hex.bytesToStringUppercase(hasher.digest()); 65 | } catch (Exception e) { 66 | throw new RuntimeException(e); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/model/UnzipEngineParameters.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.model; 18 | 19 | import java.io.FileOutputStream; 20 | 21 | import net.lingala.zip4j.crypto.IDecrypter; 22 | import net.lingala.zip4j.unzip.UnzipEngine; 23 | 24 | public class UnzipEngineParameters { 25 | 26 | private ZipModel zipModel; 27 | 28 | private FileHeader fileHeader; 29 | 30 | private LocalFileHeader localFileHeader; 31 | 32 | private IDecrypter iDecryptor; 33 | 34 | private FileOutputStream outputStream; 35 | 36 | private UnzipEngine unzipEngine; 37 | 38 | public ZipModel getZipModel() { 39 | return zipModel; 40 | } 41 | 42 | public void setZipModel(ZipModel zipModel) { 43 | this.zipModel = zipModel; 44 | } 45 | 46 | public FileHeader getFileHeader() { 47 | return fileHeader; 48 | } 49 | 50 | public void setFileHeader(FileHeader fileHeader) { 51 | this.fileHeader = fileHeader; 52 | } 53 | 54 | public LocalFileHeader getLocalFileHeader() { 55 | return localFileHeader; 56 | } 57 | 58 | public void setLocalFileHeader(LocalFileHeader localFileHeader) { 59 | this.localFileHeader = localFileHeader; 60 | } 61 | 62 | public IDecrypter getIDecryptor() { 63 | return iDecryptor; 64 | } 65 | 66 | public void setIDecryptor(IDecrypter decrypter) { 67 | iDecryptor = decrypter; 68 | } 69 | 70 | public FileOutputStream getOutputStream() { 71 | return outputStream; 72 | } 73 | 74 | public void setOutputStream(FileOutputStream outputStream) { 75 | this.outputStream = outputStream; 76 | } 77 | 78 | public UnzipEngine getUnzipEngine() { 79 | return unzipEngine; 80 | } 81 | 82 | public void setUnzipEngine(UnzipEngine unzipEngine) { 83 | this.unzipEngine = unzipEngine; 84 | } 85 | 86 | 87 | 88 | } 89 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/util/Zip4jConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.util; 18 | 19 | public interface Zip4jConstants { 20 | 21 | // Compression Types 22 | static final int COMP_STORE = 0; 23 | // static final int COMP_FILE_SHRUNK = 1; 24 | // static final int COMP_FILE_RED_COMP_FACTOR_1 = 2; 25 | // static final int COMP_FILE_RED_COMP_FACTOR_2 = 3; 26 | // static final int COMP_FILE_RED_COMP_FACTOR_3 = 4; 27 | // static final int COMP_FILE_RED_COMP_FACTOR_4 = 5; 28 | // static final int COMP_FILE_IMPLODED = 6; 29 | static final int COMP_DEFLATE = 8; 30 | // static final int COMP_FILE_ENHANCED_DEFLATED = 9; 31 | // static final int COMP_PKWARE_DATA_COMP_LIB_IMPL = 10; 32 | // static final int COMP_BZIP2 = 12; 33 | // static final int COMP_LZMA = 14; 34 | // static final int COMP_IBM_TERSE = 18; 35 | // static final int COMP_IBM_LZ77 =19; 36 | // static final int COMP_WAVPACK = 97; 37 | // static final int COMP_PPMD = 98; 38 | static final int COMP_AES_ENC = 99; 39 | 40 | //Compression level for deflate algorithm 41 | static final int DEFLATE_LEVEL_FASTEST = 1; 42 | static final int DEFLATE_LEVEL_FAST = 3; 43 | static final int DEFLATE_LEVEL_NORMAL = 5; 44 | static final int DEFLATE_LEVEL_MAXIMUM = 7; 45 | static final int DEFLATE_LEVEL_ULTRA = 9; 46 | 47 | //Encryption types 48 | static final int ENC_NO_ENCRYPTION = -1; 49 | static final int ENC_METHOD_STANDARD = 0; 50 | // static final int ENC_METHOD_STRONG = 1; 51 | static final int ENC_METHOD_AES = 99; 52 | 53 | //AES Key Strength 54 | static final int AES_STRENGTH_128 = 0x01; 55 | static final int AES_STRENGTH_192 = 0x02; 56 | static final int AES_STRENGTH_256 = 0x03; 57 | } 58 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/ditronic/securezipnotes/onlinetests/SyncTests.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.onlinetests 2 | 3 | import androidx.test.espresso.Espresso 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import androidx.test.filters.LargeTest 6 | import com.ditronic.securezipnotes.robotpattern.* 7 | import com.ditronic.securezipnotes.testutils.pressBack 8 | import com.ditronic.securezipnotes.testutils.targetContext 9 | import com.ditronic.simplefilesync.AbstractFileSync 10 | import com.ditronic.simplefilesync.DropboxFileSync 11 | import com.ditronic.simplefilesync.util.ResultCode 12 | import org.junit.Assert 13 | import org.junit.Before 14 | import org.junit.FixMethodOrder 15 | import org.junit.Test 16 | import org.junit.runner.RunWith 17 | import org.junit.runners.MethodSorters 18 | import java.util.* 19 | 20 | @RunWith(AndroidJUnit4::class) 21 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 22 | @LargeTest 23 | class SyncTests { 24 | 25 | // TODO: Assert popup messages instead of internal state 26 | 27 | companion object { 28 | private const val DROPBOX_OAUTH_TOKEN = "TODO set token via environment or property file" 29 | } 30 | 31 | @Before 32 | fun beforeEachTest() { 33 | DropboxFileSync.storeNewOauthToken(DROPBOX_OAUTH_TOKEN, targetContext()) 34 | } 35 | 36 | @Test 37 | fun dbx1_dropBoxFreshRandomUpload() { 38 | precondition_singleNote() 39 | main_clickNote("Note 1", password = TESTPASSWORD) 40 | noteEdit_typeText(UUID.randomUUID().toString()) 41 | pressBack() 42 | Espresso.onIdle() 43 | Assert.assertEquals(ResultCode.UPLOAD_SUCCESS, AbstractFileSync.getLastSyncResult()!!.resultCode) 44 | } 45 | 46 | @Test 47 | fun dbx2_dropBoxSingleNoteUpload() { 48 | precondition_singleNote() 49 | Espresso.onIdle() 50 | Assert.assertEquals(ResultCode.UPLOAD_SUCCESS, AbstractFileSync.getLastSyncResult()!!.resultCode) 51 | } 52 | 53 | @Test 54 | fun dbx3_dropBoxRemoteEqualsLocal() { 55 | precondition_singleNote() 56 | Espresso.onIdle() 57 | Assert.assertEquals(ResultCode.REMOTE_EQUALS_LOCAL, DropboxFileSync.getLastSyncResult()!!.resultCode) 58 | } 59 | 60 | @Test 61 | fun dbx4_dropBoxDownload() { 62 | precondition_cleanStart() 63 | Espresso.onIdle() 64 | Assert.assertEquals(ResultCode.DOWNLOAD_SUCCESS, DropboxFileSync.getLastSyncResult()!!.resultCode) 65 | main_assertNonEmpty() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/util/Boast.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.res.Resources.NotFoundException 6 | import android.widget.Toast 7 | import java.lang.ref.WeakReference 8 | 9 | /** 10 | * [Toast] decorator allowing for easy cancellation of notifications. Use 11 | * this class if you want subsequent Toast notifications to overwrite current 12 | * ones. 13 | */ 14 | class Boast private constructor(private val internalToast: Toast) { 15 | 16 | fun show() { 17 | val oldToast = globalBoast?.get() 18 | oldToast?.cancel() 19 | globalBoast = WeakReference(internalToast) 20 | internalToast.show() 21 | } 22 | 23 | companion object { 24 | 25 | @Volatile 26 | private var globalBoast: WeakReference? = null 27 | 28 | @SuppressLint("ShowToast") 29 | fun makeText(context: Context?, text: CharSequence?, 30 | duration: Int): Boast { 31 | return Boast(Toast.makeText(context, text, duration)) 32 | } 33 | 34 | @SuppressLint("ShowToast") 35 | @Throws(NotFoundException::class) 36 | fun makeText(context: Context?, resId: Int, duration: Int): Boast { 37 | return Boast(Toast.makeText(context, resId, duration)) 38 | } 39 | 40 | @SuppressLint("ShowToast") 41 | fun makeText(context: Context?, text: CharSequence?): Boast { 42 | return Boast(Toast.makeText(context, text, Toast.LENGTH_SHORT)) 43 | } 44 | 45 | @SuppressLint("ShowToast") 46 | @Throws(NotFoundException::class) 47 | fun makeText(context: Context?, resId: Int): Boast { 48 | return Boast(Toast.makeText(context, resId, Toast.LENGTH_SHORT)) 49 | } 50 | 51 | fun showText(context: Context?, text: CharSequence?, duration: Int) { 52 | makeText(context, text, duration).show() 53 | } 54 | 55 | @Throws(NotFoundException::class) 56 | fun showText(context: Context?, resId: Int, duration: Int) { 57 | makeText(context, resId, duration).show() 58 | } 59 | 60 | fun showText(context: Context?, text: CharSequence?) { 61 | makeText(context, text, Toast.LENGTH_SHORT).show() 62 | } 63 | 64 | @Throws(NotFoundException::class) 65 | fun showText(context: Context?, resId: Int) { 66 | makeText(context, resId, Toast.LENGTH_SHORT).show() 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_password_confirm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 14 | 18 | 19 | 26 | 27 | 34 | 35 | 38 | 47 | 48 | 49 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/menu/MenuOptions.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.menu 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.view.MenuItem 6 | import androidx.core.content.FileProvider 7 | import com.ditronic.securezipnotes.R 8 | import com.ditronic.securezipnotes.util.Boast 9 | import com.ditronic.securezipnotes.zip.CryptoZip 10 | import com.ditronic.simplefilesync.DropboxFileSync 11 | import com.ditronic.simplefilesync.util.FilesUtil 12 | import java.io.File 13 | import java.io.IOException 14 | import java.text.SimpleDateFormat 15 | import java.util.* 16 | 17 | object MenuOptions { 18 | 19 | 20 | private val exportFileName: String 21 | get() { 22 | val dateString = SimpleDateFormat("yyyy-MM-dd-HH:mm", Locale.getDefault()).format(Calendar.getInstance().timeInMillis) 23 | return dateString + "_securezipnotes.aeszip" 24 | } 25 | 26 | private fun exportZipFile(ac: Activity) { 27 | 28 | val zipNotes = CryptoZip.getMainFilePath(ac) 29 | if (!zipNotes.exists()) { 30 | Boast.makeText(ac, R.string.toast_notes_empty_export).show() 31 | return 32 | } 33 | 34 | val tmpShareFile = File(ac.cacheDir, exportFileName) 35 | tmpShareFile.delete() 36 | tmpShareFile.deleteOnExit() 37 | try { 38 | FilesUtil.copyFile(zipNotes, tmpShareFile) 39 | } catch (e: IOException) { 40 | throw RuntimeException(e) 41 | } 42 | 43 | val intent = Intent() 44 | intent.action = Intent.ACTION_SEND 45 | val shareUri = FileProvider.getUriForFile(ac, ac.applicationContext.packageName + ".provider", tmpShareFile) 46 | intent.type = "application/octet-stream" 47 | intent.putExtra(Intent.EXTRA_STREAM, shareUri) 48 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 49 | ac.startActivity(Intent.createChooser(intent, null)) // This provides a better menu than startActivity(intent) 50 | } 51 | 52 | fun onOptionsSharedItemSelected(item: MenuItem, ac: Activity): Boolean { 53 | when (item.itemId) { 54 | android.R.id.home -> { 55 | ac.onBackPressed() 56 | return true 57 | } 58 | R.id.action_export_zip_file -> { 59 | exportZipFile(ac) 60 | return true 61 | } 62 | R.id.action_sync_dropbox -> { 63 | DropboxFileSync.launchInitialOauthActivity(ac) 64 | return true 65 | } 66 | else -> return false 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Secure Zip Notes 2 | Securely store notes within an encrypted Zip file. 3 | 4 | 5 | Get it on Google Play 6 | 7 | CI status 8 | 9 | You do not trust bloated password managers with undocumented file formats? 10 | You want to retain 100% control over your data? 11 | Then Secure Zip Notes is a solution. 12 | 13 | # Deprecation Notice 14 | 15 | I deprecate this app because I believe the "XML-based UI" is no longer suitable for future development. 16 | Instead, I recommend embracing a modern declarative UI framework. 17 | Declarative UIs, popularized by frameworks like Flutter and React, are, in my opinion, vastly superior to traditional XML-UIs. 18 | However, due to other projects taking higher priority, I am unable to allocate time to rewriting this UI. 19 | 20 | Please note that notes created within this app can still be opened using independent programs (e.g., 7-Zip, WinZip, The Unarchiver for macOS, Gnome Archive Manager). 21 | 22 | ## Features 23 | - View and edit encrypted text files on any platform, using password-protected Zip files. 24 | - Optional sync with Dropbox (they cannot decrypt your data). 25 | - Simple import/export of Zip files. 26 | - Uses hardware-protected storage to avoid retyping the master password every time. 27 | - Open Source: Fetch this app from GitHub if you do not trust us. 28 | 29 | Our top priority is not only security and privacy, but also long-term stability. 30 | We take the responsibility to retain your data seriously. 31 | Secure Zip Notes guarantees that you can easily decrypt your data in 50 years even if DiTronic Apps ceases to exist. 32 | 33 | This app only supports text notes. 34 | If you are seeking advanced features like auto-fill passwords, then we recommend other apps like Keepass2Android. 35 | 36 | ## Technical details 37 | - Supported independent programs: 7-Zip, WinZip, The Unarchiver (macOS), Gnome Archive Manager 38 | - Encryption: AES-256 Counter Mode + HMAC-SHA1 39 | - Key derivation: PBKDF2 40 | 41 | Not all PC operating systems support Zip files with AES encryption by default. 42 | Therefore, you might need to install a PC software like 7-Zip. 43 | _____________________________________________________________________ 44 | 45 | ## Attributions 46 | This app uses a modified version of the Zip4j library (Apache License 2.0). 47 | 48 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/io/ZipInputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.io; 18 | 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | 22 | import net.lingala.zip4j.exception.ZipException; 23 | 24 | public class ZipInputStream extends InputStream { 25 | 26 | private BaseInputStream is; 27 | 28 | public ZipInputStream(BaseInputStream is) { 29 | this.is = is; 30 | } 31 | 32 | public int read() throws IOException { 33 | int readByte = is.read(); 34 | if (readByte != -1) { 35 | is.getUnzipEngine().updateCRC(readByte); 36 | } 37 | return readByte; 38 | } 39 | 40 | public int read(byte[] b) throws IOException { 41 | return read(b, 0, b.length); 42 | } 43 | 44 | public int read(byte[] b, int off, int len) throws IOException { 45 | int readLen = is.read(b, off, len); 46 | if (readLen > 0 && is.getUnzipEngine() != null) { 47 | is.getUnzipEngine().updateCRC(b, off, readLen); 48 | } 49 | return readLen; 50 | } 51 | 52 | /** 53 | * Closes the input stream and releases any resources. 54 | * This method also checks for the CRC of the extracted file. 55 | * If CRC check has to be skipped use close(boolean skipCRCCheck) method 56 | * 57 | * @throws IOException 58 | */ 59 | public void close() throws IOException { 60 | close(false); 61 | } 62 | 63 | /** 64 | * Closes the input stream and releases any resources. 65 | * If skipCRCCheck flag is set to true, this method skips CRC Check 66 | * of the extracted file 67 | * 68 | * @throws IOException 69 | */ 70 | public void close(boolean skipCRCCheck) throws IOException { 71 | try { 72 | is.close(); 73 | if (!skipCRCCheck && is.getUnzipEngine() != null) { 74 | is.getUnzipEngine().checkCRC(); 75 | } 76 | } catch (ZipException e) { 77 | throw new IOException(e.getMessage()); 78 | } 79 | } 80 | 81 | public int available() throws IOException { 82 | return is.available(); 83 | } 84 | 85 | public long skip(long n) throws IOException { 86 | return is.skip(n); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/model/UnzipParameters.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.model; 18 | 19 | public class UnzipParameters { 20 | 21 | private boolean ignoreReadOnlyFileAttribute; 22 | private boolean ignoreHiddenFileAttribute; 23 | private boolean ignoreArchiveFileAttribute; 24 | private boolean ignoreSystemFileAttribute; 25 | private boolean ignoreAllFileAttributes; 26 | private boolean ignoreDateTimeAttributes; 27 | 28 | public boolean isIgnoreReadOnlyFileAttribute() { 29 | return ignoreReadOnlyFileAttribute; 30 | } 31 | 32 | public void setIgnoreReadOnlyFileAttribute(boolean ignoreReadOnlyFileAttribute) { 33 | this.ignoreReadOnlyFileAttribute = ignoreReadOnlyFileAttribute; 34 | } 35 | 36 | public boolean isIgnoreHiddenFileAttribute() { 37 | return ignoreHiddenFileAttribute; 38 | } 39 | 40 | public void setIgnoreHiddenFileAttribute(boolean ignoreHiddenFileAttribute) { 41 | this.ignoreHiddenFileAttribute = ignoreHiddenFileAttribute; 42 | } 43 | 44 | public boolean isIgnoreArchiveFileAttribute() { 45 | return ignoreArchiveFileAttribute; 46 | } 47 | 48 | public void setIgnoreArchiveFileAttribute(boolean ignoreArchiveFileAttribute) { 49 | this.ignoreArchiveFileAttribute = ignoreArchiveFileAttribute; 50 | } 51 | 52 | public boolean isIgnoreSystemFileAttribute() { 53 | return ignoreSystemFileAttribute; 54 | } 55 | 56 | public void setIgnoreSystemFileAttribute(boolean ignoreSystemFileAttribute) { 57 | this.ignoreSystemFileAttribute = ignoreSystemFileAttribute; 58 | } 59 | 60 | public boolean isIgnoreAllFileAttributes() { 61 | return ignoreAllFileAttributes; 62 | } 63 | 64 | public void setIgnoreAllFileAttributes(boolean ignoreAllFileAttributes) { 65 | this.ignoreAllFileAttributes = ignoreAllFileAttributes; 66 | } 67 | 68 | public boolean isIgnoreDateTimeAttributes() { 69 | return ignoreDateTimeAttributes; 70 | } 71 | 72 | public void setIgnoreDateTimeAttributes(boolean ignoreDateTimeAttributes) { 73 | this.ignoreDateTimeAttributes = ignoreDateTimeAttributes; 74 | } 75 | 76 | 77 | } 78 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/crypto/PBKDF2/BinTools.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.crypto.PBKDF2; 18 | 19 | /* 20 | * Source referred from Matthias Gartner's PKCS#5 implementation - 21 | * see http://rtner.de/software/PBKDF2.html 22 | */ 23 | 24 | class BinTools 25 | { 26 | public static final String hex = "0123456789ABCDEF"; 27 | 28 | public static String bin2hex(final byte[] b) 29 | { 30 | if (b == null) 31 | { 32 | return ""; 33 | } 34 | StringBuffer sb = new StringBuffer(2 * b.length); 35 | for (int i = 0; i < b.length; i++) 36 | { 37 | int v = (256 + b[i]) % 256; 38 | sb.append(hex.charAt((v / 16) & 15)); 39 | sb.append(hex.charAt((v % 16) & 15)); 40 | } 41 | return sb.toString(); 42 | } 43 | 44 | public static byte[] hex2bin(final String s) 45 | { 46 | String m = s; 47 | if (s == null) 48 | { 49 | // Allow empty input string. 50 | m = ""; 51 | } 52 | else if (s.length() % 2 != 0) 53 | { 54 | // Assume leading zero for odd string length 55 | m = "0" + s; 56 | } 57 | byte r[] = new byte[m.length() / 2]; 58 | for (int i = 0, n = 0; i < m.length(); n++) 59 | { 60 | char h = m.charAt(i++); 61 | char l = m.charAt(i++); 62 | r[n] = (byte) (hex2bin(h) * 16 + hex2bin(l)); 63 | } 64 | return r; 65 | } 66 | 67 | public static int hex2bin(char c) 68 | { 69 | if (c >= '0' && c <= '9') 70 | { 71 | return (c - '0'); 72 | } 73 | if (c >= 'A' && c <= 'F') 74 | { 75 | return (c - 'A' + 10); 76 | } 77 | if (c >= 'a' && c <= 'f') 78 | { 79 | return (c - 'a' + 10); 80 | } 81 | throw new IllegalArgumentException( 82 | "Input string may only contain hex digits, but found '" + c 83 | + "'"); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/util/CRCUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.util; 18 | 19 | import java.io.File; 20 | import java.io.FileInputStream; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | import java.util.zip.CRC32; 24 | 25 | import net.lingala.zip4j.exception.ZipException; 26 | import net.lingala.zip4j.progress.ProgressMonitor; 27 | 28 | public class CRCUtil { 29 | 30 | private static final int BUF_SIZE = 1 << 14; //16384 31 | 32 | public static long computeFileCRC(String inputFile) throws ZipException { 33 | return computeFileCRC(inputFile, null); 34 | } 35 | 36 | /** 37 | * Calculates CRC of a file 38 | * @param inputFile - file for which crc has to be calculated 39 | * @return crc of the file 40 | * @throws ZipException 41 | */ 42 | public static long computeFileCRC(String inputFile, ProgressMonitor progressMonitor) throws ZipException { 43 | 44 | if (!Zip4jUtil.isStringNotNullAndNotEmpty(inputFile)) { 45 | throw new ZipException("input file is null or empty, cannot calculate CRC for the file"); 46 | } 47 | InputStream inputStream = null; 48 | try { 49 | Zip4jUtil.checkFileReadAccess(inputFile); 50 | 51 | inputStream = new FileInputStream(new File(inputFile)); 52 | 53 | byte[] buff = new byte[BUF_SIZE]; 54 | int readLen = -2; 55 | CRC32 crc32 = new CRC32(); 56 | 57 | while ((readLen = inputStream.read(buff)) != -1) { 58 | crc32.update(buff, 0, readLen); 59 | if (progressMonitor != null) { 60 | progressMonitor.updateWorkCompleted(readLen); 61 | if (progressMonitor.isCancelAllTasks()) { 62 | progressMonitor.setResult(ProgressMonitor.RESULT_CANCELLED); 63 | progressMonitor.setState(ProgressMonitor.STATE_READY); 64 | return 0; 65 | } 66 | } 67 | } 68 | 69 | return crc32.getValue(); 70 | } catch (IOException e) { 71 | throw new ZipException(e); 72 | } catch (Exception e) { 73 | throw new ZipException(e); 74 | } finally { 75 | if (inputStream != null) { 76 | try { 77 | inputStream.close(); 78 | } catch (IOException e) { 79 | throw new ZipException("error while closing the file after calculating crc"); 80 | } 81 | } 82 | } 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/dialogs/PwDialog.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.dialogs 2 | 3 | import android.app.Dialog 4 | import android.os.Bundle 5 | import android.text.InputType 6 | import android.view.WindowManager 7 | import android.view.inputmethod.EditorInfo 8 | import android.widget.EditText 9 | import androidx.appcompat.app.AlertDialog 10 | import com.ditronic.securezipnotes.password.PwManager 11 | import com.ditronic.securezipnotes.password.PwRequest 12 | import com.ditronic.securezipnotes.password.PwResult 13 | import com.ditronic.securezipnotes.zip.CryptoZip 14 | 15 | 16 | class PwDialog: ShortLifeDialogFragment() { 17 | 18 | companion object { 19 | val TAG = FragmentTag("PwDialog") 20 | } 21 | 22 | lateinit var editText: EditText 23 | 24 | override fun getFragmentTag() = TAG 25 | 26 | override fun onCreateDialog(savedInstanceState: Bundle?, state: PwRequest): Dialog { 27 | editText = EditText(requireContext()) 28 | 29 | val builder = AlertDialog.Builder(requireContext()) 30 | builder.setTitle("Master password:") 31 | editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD 32 | editText.hint = "Master password" 33 | builder.setView(editText) 34 | builder.setPositiveButton(android.R.string.ok, null) 35 | .setNeutralButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() } 36 | val dialog = builder.create() 37 | editText.setOnEditorActionListener { _, actionId, _ -> 38 | if (actionId == EditorInfo.IME_ACTION_DONE) { 39 | onPositiveButtonClick() 40 | return@setOnEditorActionListener true 41 | } 42 | false 43 | } 44 | val window = dialog.window 45 | window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) 46 | editText.requestFocus() 47 | return dialog 48 | } 49 | 50 | override fun onResume() { 51 | super.onResume() 52 | val positiveButton = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE) 53 | positiveButton.setOnClickListener { 54 | onPositiveButtonClick() 55 | } 56 | } 57 | 58 | private fun onPositiveButtonClick() { 59 | val pwRequest = fetchStateOrDie() ?: return 60 | val typedPassword = editText.text.toString() 61 | val zipStream = CryptoZip.instance(requireActivity()).isPasswordValid(pwRequest.fileHeader, typedPassword) 62 | if (zipStream != null) { 63 | editText.error = null 64 | PwManager.saveUserProvidedPassword(requireActivity(), PwResult.Success(inputStream = zipStream, password = typedPassword), pwRequest.continuation) 65 | dismiss() 66 | } else { 67 | editText.error = "Wrong password" 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 55 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_new_password.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 15 | 20 | 21 | 28 | 29 | 34 | 35 | 42 | 43 | 46 | 54 | 55 | 56 | 62 | 63 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/crypto/PBKDF2/PBKDF2Parameters.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.crypto.PBKDF2; 18 | /* 19 | * Source referred from Matthias Gartner's PKCS#5 implementation - 20 | * see http://rtner.de/software/PBKDF2.html 21 | */ 22 | public class PBKDF2Parameters 23 | { 24 | protected byte[] salt; 25 | 26 | protected int iterationCount; 27 | 28 | protected String hashAlgorithm; 29 | 30 | protected String hashCharset; 31 | 32 | protected byte[] derivedKey; 33 | 34 | public PBKDF2Parameters() 35 | { 36 | this.hashAlgorithm = null; 37 | this.hashCharset = "UTF-8"; 38 | this.salt = null; 39 | this.iterationCount = 1000; 40 | this.derivedKey = null; 41 | } 42 | 43 | public PBKDF2Parameters(String hashAlgorithm, String hashCharset, 44 | byte[] salt, int iterationCount) 45 | { 46 | this.hashAlgorithm = hashAlgorithm; 47 | this.hashCharset = hashCharset; 48 | this.salt = salt; 49 | this.iterationCount = iterationCount; 50 | this.derivedKey = null; 51 | } 52 | 53 | public PBKDF2Parameters(String hashAlgorithm, String hashCharset, 54 | byte[] salt, int iterationCount, byte[] derivedKey) 55 | { 56 | this.hashAlgorithm = hashAlgorithm; 57 | this.hashCharset = hashCharset; 58 | this.salt = salt; 59 | this.iterationCount = iterationCount; 60 | this.derivedKey = derivedKey; 61 | } 62 | 63 | public int getIterationCount() 64 | { 65 | return iterationCount; 66 | } 67 | 68 | public void setIterationCount(int iterationCount) 69 | { 70 | this.iterationCount = iterationCount; 71 | } 72 | 73 | public byte[] getSalt() 74 | { 75 | return salt; 76 | } 77 | 78 | public void setSalt(byte[] salt) 79 | { 80 | this.salt = salt; 81 | } 82 | 83 | public byte[] getDerivedKey() 84 | { 85 | return derivedKey; 86 | } 87 | 88 | public void setDerivedKey(byte[] derivedKey) 89 | { 90 | this.derivedKey = derivedKey; 91 | } 92 | 93 | public String getHashAlgorithm() 94 | { 95 | return hashAlgorithm; 96 | } 97 | 98 | public void setHashAlgorithm(String hashAlgorithm) 99 | { 100 | this.hashAlgorithm = hashAlgorithm; 101 | } 102 | 103 | public String getHashCharset() 104 | { 105 | return hashCharset; 106 | } 107 | 108 | public void setHashCharset(String hashCharset) 109 | { 110 | this.hashCharset = hashCharset; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/crypto/PBKDF2/MacBasedPRF.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.crypto.PBKDF2; 18 | 19 | import java.security.InvalidKeyException; 20 | import java.security.NoSuchAlgorithmException; 21 | import java.security.NoSuchProviderException; 22 | 23 | import javax.crypto.Mac; 24 | import javax.crypto.spec.SecretKeySpec; 25 | 26 | /* 27 | * Source referred from Matthias Gartner's PKCS#5 implementation - 28 | * see http://rtner.de/software/PBKDF2.html 29 | */ 30 | 31 | public class MacBasedPRF implements PRF 32 | { 33 | protected Mac mac; 34 | 35 | protected int hLen; 36 | 37 | protected String macAlgorithm; 38 | 39 | public MacBasedPRF(String macAlgorithm) 40 | { 41 | this.macAlgorithm = macAlgorithm; 42 | try 43 | { 44 | mac = Mac.getInstance(macAlgorithm); 45 | hLen = mac.getMacLength(); 46 | } 47 | catch (NoSuchAlgorithmException e) 48 | { 49 | throw new RuntimeException(e); 50 | } 51 | } 52 | 53 | public MacBasedPRF(String macAlgorithm, String provider) 54 | { 55 | this.macAlgorithm = macAlgorithm; 56 | try 57 | { 58 | mac = Mac.getInstance(macAlgorithm, provider); 59 | hLen = mac.getMacLength(); 60 | } 61 | catch (NoSuchAlgorithmException e) 62 | { 63 | throw new RuntimeException(e); 64 | } 65 | catch (NoSuchProviderException e) 66 | { 67 | throw new RuntimeException(e); 68 | } 69 | } 70 | 71 | public byte[] doFinal(byte[] M) 72 | { 73 | byte[] r = mac.doFinal(M); 74 | return r; 75 | } 76 | 77 | public byte[] doFinal() { 78 | byte[] r = mac.doFinal(); 79 | return r; 80 | } 81 | 82 | public int getHLen() 83 | { 84 | return hLen; 85 | } 86 | 87 | public void init(byte[] P) 88 | { 89 | try 90 | { 91 | mac.init(new SecretKeySpec(P, macAlgorithm)); 92 | } 93 | catch (InvalidKeyException e) 94 | { 95 | throw new RuntimeException(e); 96 | } 97 | } 98 | 99 | public void update(byte[] U) { 100 | 101 | try { 102 | mac.update(U); 103 | } catch (IllegalStateException e) { 104 | throw new RuntimeException(e); 105 | } 106 | 107 | } 108 | 109 | public void update (byte[] U, int start, int len) { 110 | try { 111 | mac.update(U, start, len); 112 | } catch (IllegalStateException e) { 113 | throw new RuntimeException(e); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | 12 | 13 | .idea 14 | 15 | ### Intellij ### 16 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 17 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 18 | 19 | # User-specific stuff: 20 | .idea/workspace.xml 21 | .idea/tasks.xml 22 | .idea/dictionaries 23 | .idea/vcs.xml 24 | .idea/jsLibraryMappings.xml 25 | 26 | # Sensitive or high-churn files: 27 | .idea/dataSources.ids 28 | .idea/dataSources.xml 29 | .idea/dataSources.local.xml 30 | .idea/sqlDataSources.xml 31 | .idea/dynamic.xml 32 | .idea/uiDesigner.xml 33 | 34 | # Gradle: 35 | .idea/gradle.xml 36 | .idea/libraries 37 | 38 | # Mongo Explorer plugin: 39 | .idea/mongoSettings.xml 40 | 41 | ## File-based project format: 42 | *.iws 43 | 44 | ## Plugin-specific files: 45 | 46 | # IntelliJ 47 | /out/ 48 | 49 | # mpeltonen/sbt-idea plugin 50 | .idea_modules/ 51 | 52 | # JIRA plugin 53 | atlassian-ide-plugin.xml 54 | 55 | # Crashlytics plugin (for Android Studio and IntelliJ) 56 | com_crashlytics_export_strings.xml 57 | crashlytics.properties 58 | crashlytics-build.properties 59 | fabric.properties 60 | 61 | ### Intellij Patch ### 62 | *.iml 63 | 64 | ### Android ### 65 | # Built application files 66 | *.apk 67 | *.aab 68 | *.ap_ 69 | 70 | # Files for the Dalvik VM 71 | *.dex 72 | 73 | # Java class files 74 | *.class 75 | 76 | # Generated files 77 | bin/ 78 | gen/ 79 | out/ 80 | app/release/output.json 81 | 82 | # Gradle files 83 | .gradle/ 84 | build/ 85 | 86 | # Local configuration file (sdk path, etc) 87 | local.properties 88 | 89 | # Proguard folder generated by Eclipse 90 | proguard/ 91 | 92 | # Log Files 93 | *.log 94 | 95 | # Android Studio Navigation editor temp files 96 | .navigation/ 97 | 98 | # Android Studio captures folder 99 | captures/ 100 | 101 | # Intellij 102 | *.iml 103 | 104 | # Keystore files 105 | *.jks 106 | 107 | ### Android Patch ### 108 | gen-external-apklibs 109 | 110 | 111 | ### OSX ### 112 | .DS_Store 113 | .AppleDouble 114 | .LSOverride 115 | 116 | # Icon must end with two \r 117 | Icon 118 | 119 | 120 | # Thumbnails 121 | ._* 122 | 123 | # Files that might appear in the root of a volume 124 | .DocumentRevisions-V100 125 | .fseventsd 126 | .Spotlight-V100 127 | .TemporaryItems 128 | .Trashes 129 | .VolumeIcon.icns 130 | 131 | # Directories potentially created on remote AFP share 132 | .AppleDB 133 | .AppleDesktop 134 | Network Trash Folder 135 | Temporary Items 136 | .apdisk 137 | 138 | 139 | ### Windows ### 140 | # Windows image file caches 141 | Thumbs.db 142 | ehthumbs.db 143 | 144 | # Folder config file 145 | Desktop.ini 146 | 147 | # Recycle Bin used on file shares 148 | $RECYCLE.BIN/ 149 | 150 | # Windows Installer files 151 | *.cab 152 | *.msi 153 | *.msm 154 | *.msp 155 | 156 | # Windows shortcuts 157 | *.lnk 158 | 159 | 160 | ### Linux ### 161 | *~ 162 | 163 | # temporary files which can be created if a process still has a handle open of a deleted file 164 | .fuse_hidden* 165 | 166 | # KDE directory preferences 167 | .directory 168 | 169 | # Linux trash folder which might appear on any partition or disk 170 | .Trash-* 171 | 172 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/onboarding/NewPasswordActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.onboarding 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import android.view.View 6 | import android.view.inputmethod.EditorInfo 7 | import androidx.appcompat.app.AppCompatActivity 8 | import com.ditronic.securezipnotes.databinding.ActivityNewPasswordBinding 9 | import com.ditronic.securezipnotes.util.OnThrottleClickListener 10 | import java.security.SecureRandom 11 | 12 | 13 | class NewPasswordActivity : AppCompatActivity() { 14 | 15 | private lateinit var binding: ActivityNewPasswordBinding 16 | 17 | public override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | binding = ActivityNewPasswordBinding.inflate(layoutInflater) 20 | setContentView(binding.root) 21 | 22 | val toolbar = binding.toolBar 23 | setSupportActionBar(toolbar) 24 | 25 | binding.inputPassword.setText(generatePassword()) 26 | 27 | binding.btnGenerateMasterPassword.setOnClickListener { binding.inputPassword.setText(generatePassword()) } 28 | 29 | binding.btnNext.setOnClickListener(object : OnThrottleClickListener() { 30 | public override fun onThrottleClick(v: View) { 31 | btnNext() 32 | } 33 | }) 34 | 35 | binding.inputPassword.setOnEditorActionListener { _, actionId, _ -> 36 | if (actionId == EditorInfo.IME_ACTION_DONE) { 37 | btnNext() 38 | true 39 | } else { 40 | false 41 | } 42 | } 43 | 44 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 45 | supportActionBar?.setDisplayShowHomeEnabled(true) 46 | supportActionBar?.title = "New Master Password" 47 | } 48 | 49 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 50 | return when (item.itemId) { 51 | android.R.id.home -> { 52 | onBackPressed() 53 | true 54 | } 55 | else -> super.onOptionsItemSelected(item) 56 | } 57 | } 58 | 59 | private fun btnNext() { 60 | 61 | val password = binding.inputPassword.text.toString() 62 | 63 | if (password.isEmpty() || password.length < MIN_PW_LEN) { 64 | binding.inputPassword.error = "Minimum length: $MIN_PW_LEN characters" 65 | return 66 | } 67 | binding.inputPassword.error = null 68 | PasswordConfirmActivity.launch(this, password) 69 | } 70 | 71 | companion object { 72 | private val TAG = NewPasswordActivity::class.java.name 73 | 74 | private const val ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 75 | 76 | private const val DEFAULT_PW_LEN = 20 // This is sufficient against offline attacks 77 | private const val MIN_PW_LEN = 8 // This is too short, but we won't hinder the user from shooting themselves 78 | 79 | private fun generatePassword(): String { 80 | val length = DEFAULT_PW_LEN 81 | val secureRandom = SecureRandom() 82 | val stringBuilder = StringBuilder(length) 83 | repeat(times = length) { 84 | stringBuilder.append(ALPHABET[secureRandom.nextInt(ALPHABET.length)]) 85 | } 86 | return stringBuilder.toString() 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/onboarding/PasswordConfirmActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.onboarding 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.view.MenuItem 7 | import android.view.View 8 | import android.view.WindowManager 9 | import android.view.inputmethod.EditorInfo 10 | import androidx.appcompat.app.AppCompatActivity 11 | import com.ditronic.securezipnotes.databinding.ActivityPasswordConfirmBinding 12 | import com.ditronic.securezipnotes.noteselect.MainActivity 13 | import com.ditronic.securezipnotes.password.PwManager 14 | import com.ditronic.securezipnotes.password.PwResult 15 | import com.ditronic.securezipnotes.util.OnThrottleClickListener 16 | 17 | class PasswordConfirmActivity : AppCompatActivity() { 18 | 19 | private lateinit var password: String 20 | private lateinit var binding: ActivityPasswordConfirmBinding 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | binding = ActivityPasswordConfirmBinding.inflate(layoutInflater) 25 | setContentView(binding.root) 26 | password = intent.extras!!.getString(INTENT_PASSWORD)!! 27 | 28 | val toolbar = binding.toolBar 29 | setSupportActionBar(toolbar) 30 | 31 | binding.btnConfirmMasterPassword.setOnClickListener(object : OnThrottleClickListener() { 32 | public override fun onThrottleClick(v: View) { 33 | savePassword() 34 | } 35 | }) 36 | 37 | binding.inputPasswordConfirm.setOnEditorActionListener { _, actionId, _ -> 38 | if (actionId == EditorInfo.IME_ACTION_DONE) { 39 | savePassword() 40 | return@setOnEditorActionListener true 41 | } 42 | false 43 | } 44 | 45 | // add back arrow to toolbar 46 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 47 | supportActionBar?.setDisplayShowHomeEnabled(true) 48 | supportActionBar?.title = "Confirm Master Password" 49 | 50 | val window = window 51 | window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) 52 | binding.inputPasswordConfirm.requestFocus() 53 | } 54 | 55 | private fun savePassword() { 56 | 57 | val confirmedPassword = binding.inputPasswordConfirm.text.toString() 58 | if (confirmedPassword != password) { 59 | binding.inputPasswordConfirm.error = "Passwords do not match" 60 | return 61 | } 62 | binding.inputPasswordConfirm.error = null 63 | 64 | PwManager.saveUserProvidedPassword(this, PwResult.Success(password = confirmedPassword, inputStream = null)) { 65 | MainActivity.launchCleanWithNewNote(this) 66 | } 67 | } 68 | 69 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 70 | return when (item.itemId) { 71 | android.R.id.home -> { 72 | onBackPressed() 73 | true 74 | } 75 | else -> super.onOptionsItemSelected(item) 76 | } 77 | } 78 | 79 | companion object { 80 | 81 | private const val INTENT_PASSWORD = "intent_password" 82 | 83 | fun launch(cx: Context, password: String) { 84 | val intent = Intent(cx, PasswordConfirmActivity::class.java) 85 | intent.putExtra(INTENT_PASSWORD, password) 86 | cx.startActivity(intent) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/model/EndCentralDirRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.model; 18 | 19 | public class EndCentralDirRecord { 20 | 21 | private long signature; 22 | 23 | private int noOfThisDisk; 24 | 25 | private int noOfThisDiskStartOfCentralDir; 26 | 27 | private int totNoOfEntriesInCentralDirOnThisDisk; 28 | 29 | private int totNoOfEntriesInCentralDir; 30 | 31 | private int sizeOfCentralDir; 32 | 33 | private long offsetOfStartOfCentralDir; 34 | 35 | private int commentLength; 36 | 37 | private String comment; 38 | 39 | private byte[] commentBytes; 40 | 41 | public long getSignature() { 42 | return signature; 43 | } 44 | 45 | public void setSignature(long signature) { 46 | this.signature = signature; 47 | } 48 | 49 | public int getNoOfThisDisk() { 50 | return noOfThisDisk; 51 | } 52 | 53 | public void setNoOfThisDisk(int noOfThisDisk) { 54 | this.noOfThisDisk = noOfThisDisk; 55 | } 56 | 57 | public int getNoOfThisDiskStartOfCentralDir() { 58 | return noOfThisDiskStartOfCentralDir; 59 | } 60 | 61 | public void setNoOfThisDiskStartOfCentralDir(int noOfThisDiskStartOfCentralDir) { 62 | this.noOfThisDiskStartOfCentralDir = noOfThisDiskStartOfCentralDir; 63 | } 64 | 65 | public int getTotNoOfEntriesInCentralDirOnThisDisk() { 66 | return totNoOfEntriesInCentralDirOnThisDisk; 67 | } 68 | 69 | public void setTotNoOfEntriesInCentralDirOnThisDisk( 70 | int totNoOfEntriesInCentralDirOnThisDisk) { 71 | this.totNoOfEntriesInCentralDirOnThisDisk = totNoOfEntriesInCentralDirOnThisDisk; 72 | } 73 | 74 | public int getTotNoOfEntriesInCentralDir() { 75 | return totNoOfEntriesInCentralDir; 76 | } 77 | 78 | public void setTotNoOfEntriesInCentralDir(int totNoOfEntrisInCentralDir) { 79 | this.totNoOfEntriesInCentralDir = totNoOfEntrisInCentralDir; 80 | } 81 | 82 | public int getSizeOfCentralDir() { 83 | return sizeOfCentralDir; 84 | } 85 | 86 | public void setSizeOfCentralDir(int sizeOfCentralDir) { 87 | this.sizeOfCentralDir = sizeOfCentralDir; 88 | } 89 | 90 | public long getOffsetOfStartOfCentralDir() { 91 | return offsetOfStartOfCentralDir; 92 | } 93 | 94 | public void setOffsetOfStartOfCentralDir(long offSetOfStartOfCentralDir) { 95 | this.offsetOfStartOfCentralDir = offSetOfStartOfCentralDir; 96 | } 97 | 98 | public int getCommentLength() { 99 | return commentLength; 100 | } 101 | 102 | public void setCommentLength(int commentLength) { 103 | this.commentLength = commentLength; 104 | } 105 | 106 | public String getComment() { 107 | return comment; 108 | } 109 | 110 | public void setComment(String comment) { 111 | this.comment = comment; 112 | } 113 | 114 | public byte[] getCommentBytes() { 115 | return commentBytes; 116 | } 117 | 118 | public void setCommentBytes(byte[] commentBytes) { 119 | this.commentBytes = commentBytes; 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 18 | 22 | 23 | 28 | 29 | 34 | 37 | 47 | 48 | 51 | 61 | 62 | 65 | 75 | 76 | 77 | 78 | 79 | 80 | 84 | 85 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/io/DeflaterOutputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.io; 18 | 19 | import java.io.File; 20 | import java.io.IOException; 21 | import java.io.OutputStream; 22 | import java.util.zip.Deflater; 23 | 24 | import net.lingala.zip4j.exception.ZipException; 25 | import net.lingala.zip4j.model.ZipModel; 26 | import net.lingala.zip4j.model.ZipParameters; 27 | import net.lingala.zip4j.util.InternalZipConstants; 28 | import net.lingala.zip4j.util.Zip4jConstants; 29 | 30 | public class DeflaterOutputStream extends CipherOutputStream { 31 | 32 | private byte[] buff; 33 | protected Deflater deflater; 34 | private boolean firstBytesRead; 35 | 36 | public DeflaterOutputStream(OutputStream outputStream, ZipModel zipModel) { 37 | super(outputStream, zipModel); 38 | deflater = new Deflater(); 39 | buff = new byte[InternalZipConstants.BUFF_SIZE]; 40 | firstBytesRead = false; 41 | } 42 | 43 | public void putNextEntry(File file, ZipParameters zipParameters) 44 | throws ZipException { 45 | super.putNextEntry(file, zipParameters); 46 | if (zipParameters.getCompressionMethod() == Zip4jConstants.COMP_DEFLATE) { 47 | deflater.reset(); 48 | if ((zipParameters.getCompressionLevel() < 0 || zipParameters 49 | .getCompressionLevel() > 9) 50 | && zipParameters.getCompressionLevel() != -1) { 51 | throw new ZipException( 52 | "invalid compression level for deflater. compression level should be in the range of 0-9"); 53 | } 54 | deflater.setLevel(zipParameters.getCompressionLevel()); 55 | } 56 | } 57 | 58 | public void write(byte[] b) throws IOException { 59 | write(b, 0, b.length); 60 | } 61 | 62 | private void deflate () throws IOException { 63 | int len = deflater.deflate(buff, 0, buff.length); 64 | if (len > 0) { 65 | if (deflater.finished()) { 66 | if (len == 4) return; 67 | if (len < 4) { 68 | decrementCompressedFileSize(4 - len); 69 | return; 70 | } 71 | len -= 4; 72 | } 73 | if (!firstBytesRead) { 74 | super.write(buff, 2, len - 2); 75 | firstBytesRead = true; 76 | } else { 77 | super.write(buff, 0, len); 78 | } 79 | } 80 | } 81 | 82 | public void write(int bval) throws IOException { 83 | byte[] b = new byte[1]; 84 | b[0] = (byte) bval; 85 | write(b, 0, 1); 86 | } 87 | 88 | public void write(byte[] buf, int off, int len) throws IOException { 89 | if (zipParameters.getCompressionMethod() != Zip4jConstants.COMP_DEFLATE) { 90 | super.write(buf, off, len); 91 | } else { 92 | deflater.setInput(buf, off, len); 93 | while (!deflater.needsInput()) { 94 | deflate(); 95 | } 96 | } 97 | } 98 | 99 | public void closeEntry() throws IOException, ZipException { 100 | if (zipParameters.getCompressionMethod() == Zip4jConstants.COMP_DEFLATE) { 101 | if (!deflater.finished()) { 102 | deflater.finish(); 103 | while (!deflater.finished()) { 104 | deflate(); 105 | } 106 | } 107 | firstBytesRead = false; 108 | deflater.end(); 109 | } 110 | super.closeEntry(); 111 | } 112 | 113 | public void finish() throws IOException, ZipException { 114 | super.finish(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/crypto/StandardDecrypter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.crypto; 18 | 19 | import net.lingala.zip4j.crypto.engine.ZipCryptoEngine; 20 | import net.lingala.zip4j.exception.ZipException; 21 | import net.lingala.zip4j.exception.ZipExceptionConstants; 22 | import net.lingala.zip4j.model.FileHeader; 23 | import net.lingala.zip4j.util.InternalZipConstants; 24 | 25 | public class StandardDecrypter implements IDecrypter { 26 | 27 | private FileHeader fileHeader; 28 | private byte[] crc = new byte[4]; 29 | private ZipCryptoEngine zipCryptoEngine; 30 | 31 | public StandardDecrypter(FileHeader fileHeader, byte[] headerBytes) throws ZipException{ 32 | if (fileHeader == null) { 33 | throw new ZipException("one of more of the input parameters were null in StandardDecryptor"); 34 | } 35 | 36 | this.fileHeader = fileHeader; 37 | this.zipCryptoEngine = new ZipCryptoEngine(); 38 | init(headerBytes); 39 | } 40 | 41 | public int decryptData(byte[] buff) throws ZipException { 42 | return decryptData(buff, 0, buff.length); 43 | } 44 | 45 | public int decryptData(byte[] buff, int start, int len) throws ZipException { 46 | if (start < 0 || len < 0) { 47 | throw new ZipException("one of the input parameters were null in standard decrpyt data"); 48 | } 49 | 50 | try { 51 | for (int i = start; i < start + len; i++) { 52 | int val = buff[i] & 0xff; 53 | val = (val ^ zipCryptoEngine.decryptByte()) & 0xff; 54 | zipCryptoEngine.updateKeys((byte) val); 55 | buff[i] = (byte)val; 56 | } 57 | return len; 58 | } catch (Exception e) { 59 | throw new ZipException(e); 60 | } 61 | } 62 | 63 | public void init(byte[] headerBytes) throws ZipException { 64 | byte[] crcBuff = fileHeader.getCrcBuff(); 65 | crc[3] = (byte) (crcBuff[3] & 0xFF); 66 | crc[2] = (byte) ((crcBuff[3] >> 8) & 0xFF); 67 | crc[1] = (byte) ((crcBuff[3] >> 16) & 0xFF); 68 | crc[0] = (byte) ((crcBuff[3] >> 24) & 0xFF); 69 | 70 | if(crc[2] > 0 || crc[1] > 0 || crc[0] > 0) 71 | throw new IllegalStateException("Invalid CRC in File Header"); 72 | 73 | if (fileHeader.getPassword() == null || fileHeader.getPassword().length <= 0) { 74 | throw new ZipException("Wrong password!", ZipExceptionConstants.WRONG_PASSWORD); 75 | } 76 | 77 | zipCryptoEngine.initKeys(fileHeader.getPassword()); 78 | 79 | try { 80 | int result = headerBytes[0]; 81 | for (int i = 0; i < InternalZipConstants.STD_DEC_HDR_SIZE; i++) { 82 | // Commented this as this check cannot always be trusted 83 | // New functionality: If there is an error in extracting a password protected file, 84 | // "Wrong Password?" text is appended to the exception message 85 | // if(i+1 == InternalZipConstants.STD_DEC_HDR_SIZE && ((byte)(result ^ zipCryptoEngine.decryptByte()) != crc[3]) && !isSplit) 86 | // throw new ZipException("Wrong password!", ZipExceptionConstants.WRONG_PASSWORD); 87 | 88 | zipCryptoEngine.updateKeys((byte) (result ^ zipCryptoEngine.decryptByte())); 89 | if (i+1 != InternalZipConstants.STD_DEC_HDR_SIZE) 90 | result = headerBytes[i+1]; 91 | } 92 | } catch (Exception e) { 93 | throw new ZipException(e); 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'com.google.firebase.crashlytics' 5 | 6 | android { 7 | compileSdkVersion 30 8 | defaultConfig { 9 | applicationId "com.ditronic.securezipnotes" 10 | minSdkVersion 19 11 | targetSdkVersion 30 12 | versionCode 7 13 | versionName "1.2.0" 14 | multiDexEnabled true 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | // Causes a "pm clear" command after each test invocation 17 | testInstrumentationRunnerArguments clearPackageData: 'true' 18 | 19 | setProperty("archivesBaseName", applicationId + "-v" + versionCode + "(" + versionName + ")") 20 | } 21 | buildTypes { 22 | release { 23 | minifyEnabled true 24 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 25 | manifestPlaceholders = [enableCrashlyticsReporting: "true"] 26 | } 27 | debug { 28 | // A different "package name" for debug builds enables a clean separation during development. 29 | applicationIdSuffix = ".dev" 30 | versionNameSuffix '-DEBUG' 31 | minifyEnabled false 32 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 33 | manifestPlaceholders = [enableCrashlyticsReporting: "false"] 34 | } 35 | } 36 | lintOptions { 37 | abortOnError true 38 | warningsAsErrors true 39 | lintConfig file('lint.xml') 40 | } 41 | compileOptions { 42 | targetCompatibility = '1.8' 43 | sourceCompatibility = '1.8' 44 | kotlinOptions.allWarningsAsErrors = false 45 | } 46 | kotlinOptions { 47 | jvmTarget = "1.8" 48 | } 49 | testOptions { 50 | execution 'ANDROIDX_TEST_ORCHESTRATOR' 51 | } 52 | packagingOptions { 53 | exclude 'META-INF/DEPENDENCIES' 54 | } 55 | viewBinding.enabled = true 56 | } 57 | 58 | dependencies { 59 | implementation fileTree(dir: 'libs', include: ['*.jar']) 60 | implementation 'androidx.appcompat:appcompat:1.2.0' 61 | //implementation 'com.android.support:multidex:1.0.3' 62 | 63 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 64 | implementation 'com.google.android.material:material:1.3.0-beta01' 65 | implementation 'androidx.cardview:cardview:1.0.0' 66 | implementation "com.jakewharton.timber:timber:4.7.1" 67 | 68 | implementation 'androidx.biometric:biometric:1.0.1' 69 | //implementation 'com.google.android.gms:play-services-ads:18.0.0' 70 | implementation 'com.google.firebase:firebase-analytics:18.0.0' 71 | implementation 'com.google.firebase:firebase-crashlytics:17.3.0' 72 | 73 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' 74 | 75 | testImplementation 'junit:junit:4.13.1' 76 | 77 | androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 78 | androidTestImplementation 'androidx.test:core-ktx:1.3.1-alpha02' 79 | androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3-alpha02' 80 | androidTestImplementation 'androidx.test:rules:1.3.1-alpha02' 81 | androidTestImplementation 'androidx.test:runner:1.3.1-alpha02' 82 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha02' 83 | androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0' 84 | 85 | androidTestUtil 'androidx.test:orchestrator:1.3.0' 86 | 87 | implementation project(path: ':simplefilesync') 88 | implementation project(path: ':zip4j') 89 | implementation 'androidx.core:core-ktx:1.5.0-alpha05' 90 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 91 | } 92 | repositories { 93 | mavenCentral() 94 | } 95 | 96 | apply plugin: 'com.google.gms.google-services' 97 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/model/Zip64EndCentralDirRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.model; 18 | 19 | public class Zip64EndCentralDirRecord { 20 | 21 | private long signature; 22 | 23 | private long sizeOfZip64EndCentralDirRec; 24 | 25 | private int versionMadeBy; 26 | 27 | private int versionNeededToExtract; 28 | 29 | private int noOfThisDisk; 30 | 31 | private int noOfThisDiskStartOfCentralDir; 32 | 33 | private long totNoOfEntriesInCentralDirOnThisDisk; 34 | 35 | private long totNoOfEntriesInCentralDir; 36 | 37 | private long sizeOfCentralDir; 38 | 39 | private long offsetStartCenDirWRTStartDiskNo; 40 | 41 | private byte[] extensibleDataSector; 42 | 43 | public long getSignature() { 44 | return signature; 45 | } 46 | 47 | public void setSignature(long signature) { 48 | this.signature = signature; 49 | } 50 | 51 | public long getSizeOfZip64EndCentralDirRec() { 52 | return sizeOfZip64EndCentralDirRec; 53 | } 54 | 55 | public void setSizeOfZip64EndCentralDirRec(long sizeOfZip64EndCentralDirRec) { 56 | this.sizeOfZip64EndCentralDirRec = sizeOfZip64EndCentralDirRec; 57 | } 58 | 59 | public int getVersionMadeBy() { 60 | return versionMadeBy; 61 | } 62 | 63 | public void setVersionMadeBy(int versionMadeBy) { 64 | this.versionMadeBy = versionMadeBy; 65 | } 66 | 67 | public int getVersionNeededToExtract() { 68 | return versionNeededToExtract; 69 | } 70 | 71 | public void setVersionNeededToExtract(int versionNeededToExtract) { 72 | this.versionNeededToExtract = versionNeededToExtract; 73 | } 74 | 75 | public int getNoOfThisDisk() { 76 | return noOfThisDisk; 77 | } 78 | 79 | public void setNoOfThisDisk(int noOfThisDisk) { 80 | this.noOfThisDisk = noOfThisDisk; 81 | } 82 | 83 | public int getNoOfThisDiskStartOfCentralDir() { 84 | return noOfThisDiskStartOfCentralDir; 85 | } 86 | 87 | public void setNoOfThisDiskStartOfCentralDir(int noOfThisDiskStartOfCentralDir) { 88 | this.noOfThisDiskStartOfCentralDir = noOfThisDiskStartOfCentralDir; 89 | } 90 | 91 | public long getTotNoOfEntriesInCentralDirOnThisDisk() { 92 | return totNoOfEntriesInCentralDirOnThisDisk; 93 | } 94 | 95 | public void setTotNoOfEntriesInCentralDirOnThisDisk( 96 | long totNoOfEntriesInCentralDirOnThisDisk) { 97 | this.totNoOfEntriesInCentralDirOnThisDisk = totNoOfEntriesInCentralDirOnThisDisk; 98 | } 99 | 100 | public long getTotNoOfEntriesInCentralDir() { 101 | return totNoOfEntriesInCentralDir; 102 | } 103 | 104 | public void setTotNoOfEntriesInCentralDir(long totNoOfEntriesInCentralDir) { 105 | this.totNoOfEntriesInCentralDir = totNoOfEntriesInCentralDir; 106 | } 107 | 108 | public long getSizeOfCentralDir() { 109 | return sizeOfCentralDir; 110 | } 111 | 112 | public void setSizeOfCentralDir(long sizeOfCentralDir) { 113 | this.sizeOfCentralDir = sizeOfCentralDir; 114 | } 115 | 116 | public long getOffsetStartCenDirWRTStartDiskNo() { 117 | return offsetStartCenDirWRTStartDiskNo; 118 | } 119 | 120 | public void setOffsetStartCenDirWRTStartDiskNo( 121 | long offsetStartCenDirWRTStartDiskNo) { 122 | this.offsetStartCenDirWRTStartDiskNo = offsetStartCenDirWRTStartDiskNo; 123 | } 124 | 125 | public byte[] getExtensibleDataSector() { 126 | return extensibleDataSector; 127 | } 128 | 129 | public void setExtensibleDataSector(byte[] extensibleDataSector) { 130 | this.extensibleDataSector = extensibleDataSector; 131 | } 132 | 133 | 134 | 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/noteedit/NoteEditUI.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.noteedit 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.view.ActionMode 6 | import android.view.Menu 7 | import android.view.MenuItem 8 | import android.view.WindowManager 9 | import android.view.inputmethod.InputMethodManager 10 | import kotlinx.android.synthetic.main.activity_note_edit.* 11 | 12 | 13 | fun NoteEditActivity.applyEditMode(enable: Boolean) { 14 | model.editMode = enable 15 | val editTextTitle = edit_text_title 16 | val editTextMain = edit_text_main 17 | 18 | // Rather simple procedure for title edit text 19 | editTextTitle.isCursorVisible = model.editMode 20 | editTextTitle.isClickable = model.editMode 21 | editTextTitle.isFocusable = model.editMode 22 | editTextTitle.isLongClickable = model.editMode 23 | editTextTitle.setTextIsSelectable(model.editMode) 24 | editTextTitle.isLongClickable = model.editMode 25 | 26 | 27 | // Complicated procedure for the main edit text 28 | if (Build.VERSION.SDK_INT >= NoteEditActivity.MIN_API_COPY_READ_ONLY) { // 21 29 | editTextMain.showSoftInputOnFocus = model.editMode 30 | editTextMain.isCursorVisible = model.editMode 31 | } else { 32 | editTextMain.isCursorVisible = model.editMode 33 | editTextMain.isClickable = model.editMode 34 | editTextMain.isFocusable = model.editMode 35 | editTextMain.isLongClickable = model.editMode 36 | editTextMain.setTextIsSelectable(model.editMode) 37 | editTextMain.isLongClickable = model.editMode 38 | } 39 | 40 | if (!model.editMode) { 41 | editTextMain.customSelectionActionModeCallback = CustomSelectionActionModeCallback() 42 | if (Build.VERSION.SDK_INT >= NoteEditActivity.MIN_API_COPY_READ_ONLY) { // 23 43 | editTextMain.customInsertionActionModeCallback = CustomInsertionActionModeCallback() 44 | } 45 | // Close keyboard 46 | window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) 47 | val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 48 | imm.hideSoftInputFromWindow(editTextMain.windowToken, 0) 49 | } else { 50 | editTextMain.customSelectionActionModeCallback = null 51 | if (Build.VERSION.SDK_INT >= NoteEditActivity.MIN_API_COPY_READ_ONLY) { // 23 52 | editTextMain.customInsertionActionModeCallback = null 53 | } 54 | // Open keyboard 55 | editTextMain.requestFocus() 56 | window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) 57 | val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 58 | imm.showSoftInput(editTextMain, InputMethodManager.SHOW_IMPLICIT) 59 | } 60 | 61 | invalidateOptionsMenu() 62 | } 63 | 64 | 65 | internal class CustomSelectionActionModeCallback : ActionMode.Callback { 66 | override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { 67 | return true 68 | } 69 | 70 | override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { 71 | try { 72 | val copyItem = menu.findItem(android.R.id.copy) 73 | val title = copyItem.title 74 | menu.clear() // We only want copy functionality, no paste, no cut. 75 | menu.add(0, android.R.id.copy, 0, title) 76 | } catch (ignored: Exception) { 77 | } 78 | 79 | return true 80 | } 81 | 82 | override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { 83 | return false 84 | } 85 | 86 | override fun onDestroyActionMode(mode: ActionMode) {} 87 | } 88 | 89 | internal class CustomInsertionActionModeCallback : ActionMode.Callback { 90 | override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { 91 | return false 92 | } 93 | 94 | override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { 95 | return false 96 | } 97 | 98 | override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { 99 | return false 100 | } 101 | 102 | override fun onDestroyActionMode(mode: ActionMode) {} 103 | } 104 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/unzip/UnzipUtil.java: -------------------------------------------------------------------------------- 1 | package net.lingala.zip4j.unzip; 2 | 3 | import java.io.File; 4 | 5 | import net.lingala.zip4j.exception.ZipException; 6 | import net.lingala.zip4j.model.FileHeader; 7 | import net.lingala.zip4j.model.UnzipParameters; 8 | import net.lingala.zip4j.util.InternalZipConstants; 9 | import net.lingala.zip4j.util.Zip4jUtil; 10 | 11 | public class UnzipUtil { 12 | 13 | public static void applyFileAttributes(FileHeader fileHeader, File file) throws ZipException { 14 | applyFileAttributes(fileHeader, file, null); 15 | } 16 | 17 | public static void applyFileAttributes(FileHeader fileHeader, File file, 18 | UnzipParameters unzipParameters) throws ZipException{ 19 | 20 | if (fileHeader == null) { 21 | throw new ZipException("cannot set file properties: file header is null"); 22 | } 23 | 24 | if (file == null) { 25 | throw new ZipException("cannot set file properties: output file is null"); 26 | } 27 | 28 | if (!Zip4jUtil.checkFileExists(file)) { 29 | throw new ZipException("cannot set file properties: file doesnot exist"); 30 | } 31 | 32 | if (unzipParameters == null || !unzipParameters.isIgnoreDateTimeAttributes()) { 33 | setFileLastModifiedTime(fileHeader, file); 34 | } 35 | 36 | if (unzipParameters == null) { 37 | setFileAttributes(fileHeader, file, true, true, true, true); 38 | } else { 39 | if (unzipParameters.isIgnoreAllFileAttributes()) { 40 | setFileAttributes(fileHeader, file, false, false, false, false); 41 | } else { 42 | setFileAttributes(fileHeader, file, !unzipParameters.isIgnoreReadOnlyFileAttribute(), 43 | !unzipParameters.isIgnoreHiddenFileAttribute(), 44 | !unzipParameters.isIgnoreArchiveFileAttribute(), 45 | !unzipParameters.isIgnoreSystemFileAttribute()); 46 | } 47 | } 48 | } 49 | 50 | private static void setFileAttributes(FileHeader fileHeader, File file, boolean setReadOnly, 51 | boolean setHidden, boolean setArchive, boolean setSystem) throws ZipException { 52 | if (fileHeader == null) { 53 | throw new ZipException("invalid file header. cannot set file attributes"); 54 | } 55 | 56 | byte[] externalAttrbs = fileHeader.getExternalFileAttr(); 57 | if (externalAttrbs == null) { 58 | return; 59 | } 60 | 61 | int atrrib = externalAttrbs[0]; 62 | switch (atrrib) { 63 | case InternalZipConstants.FILE_MODE_READ_ONLY: 64 | if (setReadOnly) Zip4jUtil.setFileReadOnly(file); 65 | break; 66 | case InternalZipConstants.FILE_MODE_HIDDEN: 67 | case InternalZipConstants.FOLDER_MODE_HIDDEN: 68 | if (setHidden) Zip4jUtil.setFileHidden(file); 69 | break; 70 | case InternalZipConstants.FILE_MODE_ARCHIVE: 71 | case InternalZipConstants.FOLDER_MODE_ARCHIVE: 72 | if (setArchive) Zip4jUtil.setFileArchive(file); 73 | break; 74 | case InternalZipConstants.FILE_MODE_READ_ONLY_HIDDEN: 75 | if (setReadOnly) Zip4jUtil.setFileReadOnly(file); 76 | if (setHidden) Zip4jUtil.setFileHidden(file); 77 | break; 78 | case InternalZipConstants.FILE_MODE_READ_ONLY_ARCHIVE: 79 | if (setArchive) Zip4jUtil.setFileArchive(file); 80 | if (setReadOnly) Zip4jUtil.setFileReadOnly(file); 81 | break; 82 | case InternalZipConstants.FILE_MODE_HIDDEN_ARCHIVE: 83 | case InternalZipConstants.FOLDER_MODE_HIDDEN_ARCHIVE: 84 | if (setArchive) Zip4jUtil.setFileArchive(file); 85 | if (setHidden) Zip4jUtil.setFileHidden(file); 86 | break; 87 | case InternalZipConstants.FILE_MODE_READ_ONLY_HIDDEN_ARCHIVE: 88 | if (setArchive) Zip4jUtil.setFileArchive(file); 89 | if (setReadOnly) Zip4jUtil.setFileReadOnly(file); 90 | if (setHidden) Zip4jUtil.setFileHidden(file); 91 | break; 92 | case InternalZipConstants.FILE_MODE_SYSTEM: 93 | if (setReadOnly) Zip4jUtil.setFileReadOnly(file); 94 | if (setHidden) Zip4jUtil.setFileHidden(file); 95 | if (setSystem) Zip4jUtil.setFileSystemMode(file); 96 | break; 97 | default: 98 | //do nothing 99 | break; 100 | } 101 | } 102 | 103 | private static void setFileLastModifiedTime(FileHeader fileHeader, File file) throws ZipException { 104 | if (fileHeader.getLastModFileTime() <= 0) { 105 | return; 106 | } 107 | 108 | if (file.exists()) { 109 | file.setLastModified(Zip4jUtil.dosToJavaTme(fileHeader.getLastModFileTime())); 110 | } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/ditronic/securezipnotes/tests/SetupTests.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.tests 2 | 3 | import androidx.test.espresso.assertion.ViewAssertions 4 | import androidx.test.espresso.matcher.ViewMatchers 5 | import androidx.test.espresso.matcher.ViewMatchers.assertThat 6 | import androidx.test.ext.junit.runners.AndroidJUnit4 7 | import androidx.test.filters.LargeTest 8 | import com.ditronic.securezipnotes.robotpattern.* 9 | import com.ditronic.securezipnotes.testutils.pressBack 10 | import org.hamcrest.Description 11 | import org.hamcrest.Matcher 12 | import org.hamcrest.Matchers 13 | import org.hamcrest.Matchers.containsString 14 | import org.hamcrest.TypeSafeMatcher 15 | import org.junit.Assert.assertEquals 16 | import org.junit.Test 17 | import org.junit.runner.RunWith 18 | import java.text.SimpleDateFormat 19 | 20 | 21 | @RunWith(AndroidJUnit4::class) 22 | @LargeTest 23 | class SetupTests { 24 | 25 | companion object { 26 | private const val SECRET_NOTE = "My secret note" 27 | 28 | private fun matchesRandomPassword(): Matcher = object : TypeSafeMatcher() { 29 | override fun describeTo(description: Description?) = Unit 30 | override fun matchesSafely(password: String): Boolean { 31 | if (password.length != 20) { 32 | return false 33 | } 34 | return true 35 | } 36 | } 37 | } 38 | 39 | @Test 40 | fun createNewPassword() { 41 | precondition_cleanStart() 42 | 43 | init_createNewZipFile() 44 | 45 | init_genRandomPassword() 46 | init_typeNewPassword(TESTPASSWORD) 47 | init_confirmNewPassword(TESTPASSWORD) 48 | 49 | noteEdit_typeText(SECRET_NOTE) 50 | noteEdit_assertState("Note 1", SECRET_NOTE, editMode = true) 51 | pressBack() 52 | 53 | main_assertListState(listOf("Note 1")) 54 | val noteEntry = main_extractEntryList()[0] 55 | assertEquals("Size: " + SECRET_NOTE.length, noteEntry.size) 56 | assertThat(noteEntry.modDate, containsString(SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault()).format(java.util.Calendar.getInstance().timeInMillis))) 57 | 58 | main_clickNote("Note 1") 59 | noteEdit_assertState("Note 1", SECRET_NOTE, editMode = false) 60 | pressBack() 61 | 62 | // Back to MainActivity, assert that nothing changed 63 | main_assertListState(listOf("Note 1")) 64 | val newNoteEntry = main_extractEntryList()[0] 65 | assertEquals(noteEntry, newNoteEntry) 66 | } 67 | 68 | @Test 69 | fun passwordTooShort() { 70 | precondition_cleanStart() 71 | 72 | init_createNewZipFile() 73 | init_typeNewPassword("") 74 | init_newPassword_assertErrorText("Minimum length: 8 characters") 75 | init_typeNewPassword("sfse") 76 | init_newPassword_assertErrorText("Minimum length: 8 characters") 77 | init_typeNewPassword(TESTPASSWORD) 78 | Thread.sleep(2000) 79 | init_confirmNewPassword(TESTPASSWORD) 80 | noteEdit_assertState("Note 1", "", editMode = true) 81 | } 82 | 83 | @Test 84 | fun passwordMismatch() { 85 | precondition_cleanStart() 86 | 87 | init_createNewZipFile() 88 | init_typeNewPassword(TESTPASSWORD) 89 | init_confirmNewPassword(TESTPASSWORD + "mismatch") 90 | init_confirmPassword_assertErrorText("Passwords do not match") 91 | pressBack() 92 | pressBack() 93 | init_typeNewPassword("lalalalalala") 94 | init_confirmNewPassword("lalalalalala mismatch") 95 | init_confirmPassword_assertErrorText("Passwords do not match") 96 | init_confirmNewPassword("lalalalalala") 97 | noteEdit_assertState("Note 1", "", editMode = true) 98 | } 99 | 100 | 101 | 102 | @Test 103 | fun generateRandomPassword() { 104 | precondition_cleanStart() 105 | 106 | init_createNewZipFile() 107 | init_onViewPassword().check(ViewAssertions.matches(ViewMatchers.withText(matchesRandomPassword()))) 108 | init_typeNewPassword("") 109 | init_onViewPassword().check(ViewAssertions.matches(ViewMatchers.withText(Matchers.isEmptyString()))) 110 | init_genRandomPassword() 111 | init_onViewPassword().check(ViewAssertions.matches(ViewMatchers.withText(matchesRandomPassword()))) 112 | init_chooseNewPassword() 113 | init_confirmNewPassword("") 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/crypto/StandardEncrypter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.crypto; 18 | 19 | import java.util.Random; 20 | 21 | import net.lingala.zip4j.crypto.engine.ZipCryptoEngine; 22 | import net.lingala.zip4j.exception.ZipException; 23 | import net.lingala.zip4j.util.InternalZipConstants; 24 | 25 | public class StandardEncrypter implements IEncrypter { 26 | 27 | private ZipCryptoEngine zipCryptoEngine; 28 | private byte[] headerBytes; 29 | 30 | public StandardEncrypter(char[] password, int crc) throws ZipException { 31 | if (password == null || password.length <= 0) { 32 | throw new ZipException("input password is null or empty in standard encrpyter constructor"); 33 | } 34 | 35 | this.zipCryptoEngine = new ZipCryptoEngine(); 36 | 37 | this.headerBytes = new byte[InternalZipConstants.STD_DEC_HDR_SIZE]; 38 | init(password, crc); 39 | } 40 | 41 | private void init(char[] password, int crc) throws ZipException { 42 | if (password == null || password.length <= 0) { 43 | throw new ZipException("input password is null or empty, cannot initialize standard encrypter"); 44 | } 45 | zipCryptoEngine.initKeys(password); 46 | headerBytes = generateRandomBytes(InternalZipConstants.STD_DEC_HDR_SIZE); 47 | // Initialize again since the generated bytes were encrypted. 48 | zipCryptoEngine.initKeys(password); 49 | 50 | headerBytes[InternalZipConstants.STD_DEC_HDR_SIZE - 1] = (byte)((crc >>> 24)); 51 | headerBytes[InternalZipConstants.STD_DEC_HDR_SIZE - 2] = (byte)((crc >>> 16)); 52 | 53 | if (headerBytes.length < InternalZipConstants.STD_DEC_HDR_SIZE) { 54 | throw new ZipException("invalid header bytes generated, cannot perform standard encryption"); 55 | } 56 | 57 | encryptData(headerBytes); 58 | } 59 | 60 | public int encryptData(byte[] buff) throws ZipException { 61 | if (buff == null) { 62 | throw new NullPointerException(); 63 | } 64 | return encryptData(buff, 0, buff.length); 65 | } 66 | 67 | public int encryptData(byte[] buff, int start, int len) throws ZipException { 68 | 69 | if (len < 0) { 70 | throw new ZipException("invalid length specified to decrpyt data"); 71 | } 72 | 73 | try { 74 | for (int i = start; i < start + len; i++) { 75 | buff[i] = encryptByte(buff[i]); 76 | } 77 | return len; 78 | } catch (Exception e) { 79 | throw new ZipException(e); 80 | } 81 | } 82 | 83 | protected byte encryptByte(byte val) { 84 | byte temp_val = (byte) (val ^ zipCryptoEngine.decryptByte() & 0xff); 85 | zipCryptoEngine.updateKeys(val); 86 | return temp_val; 87 | } 88 | 89 | protected byte[] generateRandomBytes(int size) throws ZipException { 90 | 91 | if (size <= 0) { 92 | throw new ZipException("size is either 0 or less than 0, cannot generate header for standard encryptor"); 93 | } 94 | 95 | byte[] buff = new byte[size]; 96 | 97 | Random rand = new Random(); 98 | 99 | for (int i = 0; i < buff.length; i ++) { 100 | // Encrypted to get less predictability for poorly implemented 101 | // rand functions. 102 | buff[i] = encryptByte((byte) rand.nextInt(256)); 103 | } 104 | 105 | // buff[0] = (byte)87; 106 | // buff[1] = (byte)176; 107 | // buff[2] = (byte)-49; 108 | // buff[3] = (byte)-43; 109 | // buff[4] = (byte)93; 110 | // buff[5] = (byte)-204; 111 | // buff[6] = (byte)-105; 112 | // buff[7] = (byte)213; 113 | // buff[8] = (byte)-80; 114 | // buff[9] = (byte)-8; 115 | // buff[10] = (byte)21; 116 | // buff[11] = (byte)242; 117 | 118 | // for( int j=0; j<2; j++ ) { 119 | // Random rand = new Random(); 120 | // int i = rand.nextInt(); 121 | // buff[0+j*4] = (byte)(i>>24); 122 | // buff[1+j*4] = (byte)(i>>16); 123 | // buff[2+j*4] = (byte)(i>>8); 124 | // buff[3+j*4] = (byte)i; 125 | // } 126 | return buff; 127 | } 128 | 129 | public byte[] getHeaderBytes() { 130 | return headerBytes; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/ditronic/securezipnotes/testutils/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.testutils 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.view.InputDevice 7 | import android.view.MotionEvent 8 | import android.widget.Button 9 | import androidx.annotation.IdRes 10 | import androidx.appcompat.app.AlertDialog 11 | import androidx.fragment.app.DialogFragment 12 | import androidx.test.espresso.Espresso 13 | import androidx.test.espresso.ViewAction 14 | import androidx.test.espresso.action.* 15 | import androidx.test.espresso.assertion.ViewAssertions 16 | import androidx.test.espresso.matcher.RootMatchers 17 | import androidx.test.espresso.matcher.ViewMatchers 18 | import androidx.test.platform.app.InstrumentationRegistry 19 | import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry 20 | import androidx.test.runner.lifecycle.Stage 21 | import com.ditronic.securezipnotes.BuildConfig 22 | import org.hamcrest.CoreMatchers 23 | import java.io.File 24 | 25 | fun targetContext() : Context { 26 | return InstrumentationRegistry.getInstrumentation().targetContext 27 | } 28 | 29 | fun getResourceString(@IdRes resId: Int): String { 30 | return targetContext() .getString(resId) 31 | } 32 | 33 | fun assertToast(toastMessage : String) { 34 | val ac = getCurrentActivity() 35 | //Espresso.onView(ViewMatchers.withText(toastMessage)).inRoot(RootMatchers.withDecorView(CoreMatchers.not(CoreMatchers.`is`(ac.window.getDecorView())))).check(ViewAssertions.matches(ViewMatchers.isDisplayed())) 36 | Espresso.onView(ViewMatchers.withText(toastMessage)).inRoot(RootMatchers.withDecorView(CoreMatchers.not(ac.window.getDecorView()))).check(ViewAssertions.matches(ViewMatchers.isDisplayed())) 37 | } 38 | 39 | fun pressBack() { 40 | Espresso.onView(ViewMatchers.isRoot()).perform(ViewActions.pressBack()) 41 | } 42 | 43 | fun click_dialogOK() { 44 | Espresso.onView(ViewMatchers.withText("OK")).inRoot(RootMatchers.isDialog()).perform(ViewActions.click()) 45 | } 46 | 47 | fun clickBottomCenter(): ViewAction { 48 | return ViewActions.actionWithAssertions( 49 | GeneralClickAction( 50 | Tap.SINGLE, 51 | GeneralLocation.BOTTOM_CENTER, 52 | Press.FINGER, 53 | InputDevice.SOURCE_UNKNOWN, 54 | MotionEvent.BUTTON_PRIMARY)) 55 | } 56 | 57 | 58 | fun clearLocalFilesDir() { 59 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 60 | val root = appContext.filesDir 61 | val files = root.listFiles() 62 | files?.forEach { 63 | println("Remove file prior to UI test: " + it.absolutePath) 64 | if (it.delete()) { 65 | throw RuntimeException("Failed to delete from filesDir") 66 | } 67 | } 68 | } 69 | 70 | fun removeAllPrefs() { 71 | val targetContext = InstrumentationRegistry.getInstrumentation().targetContext 72 | val root = targetContext.filesDir.parentFile 73 | val sharedPrefs = File(root, "shared_prefs").list() 74 | if (sharedPrefs == null) { 75 | println("Failed to list shared preference files") 76 | return 77 | } 78 | sharedPrefs.forEach { fileName -> 79 | targetContext.getSharedPreferences(fileName.replace(".xml", ""), Context.MODE_PRIVATE).edit().clear().apply() 80 | } 81 | } 82 | 83 | fun getCurrentActivity(): Activity { 84 | lateinit var currentActivity: Activity 85 | InstrumentationRegistry.getInstrumentation().runOnMainSync { 86 | run { 87 | currentActivity = getCurrentActivityMainThread() 88 | } 89 | } 90 | return currentActivity 91 | } 92 | 93 | fun getCurrentActivityMainThread(): Activity { 94 | val currentActivity = 95 | ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED) 96 | .elementAt(0) 97 | return currentActivity 98 | } 99 | 100 | internal fun launchActivity(activityClass : Class) { 101 | val intent = Intent(Intent.ACTION_MAIN) 102 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 103 | intent.setClassName(BuildConfig.APPLICATION_ID, activityClass.name) 104 | InstrumentationRegistry.getInstrumentation().startActivitySync(intent) 105 | Espresso.onIdle() 106 | } 107 | 108 | fun clickDialogOkWithoutEspresso(dialog: DialogFragment) { 109 | val positiveButton = (dialog.dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE) 110 | clickButtonWithoutEspresso(button = positiveButton) 111 | } 112 | 113 | fun clickButtonWithoutEspresso(button: Button) { 114 | InstrumentationRegistry.getInstrumentation().runOnMainSync { 115 | button.performClick() 116 | } 117 | Espresso.onIdle() 118 | } 119 | -------------------------------------------------------------------------------- /simplefilesync/README.md: -------------------------------------------------------------------------------- 1 | # Deprecation Notice 2 | 3 | I deprecated this library because it does not reflect good Android-practices. 4 | Instead, I recommend to use Kotlin-Coroutines and ViewModels. 5 | This helps to prevent numerous memory leaks and lifecycle bugs. 6 | Or alternatively, I recommend to abandon native app-development and switch to cross-platform frameworks like https://capacitorjs.com/. 7 | 8 | ____ 9 | 10 | # Simple File Sync 11 | 12 | ``SimpleFileSync`` is an Android library that enables to easily synchronize individual files with Dropbox. 13 | 14 | ``SimpleFileSync`` provides synchronization in both directions, that is: It either uploads or downloads files. 15 | The direction depends on the timestamps. 16 | Moreover, ``SimpleFileSync`` compares a hash of the local file with the remote file to avoid unnecessary uploads or downloads. 17 | 18 | 19 | ``SimpleFileSync`` is a small wrapper on top of the following official client libraries: 20 | 21 | - Dropbox Core SDK: `com.dropbox.core:dropbox-core-sdk`. 22 | The Dropbox Core SDK is open-sourced at https://github.com/dropbox/dropbox-sdk-java. 23 | 24 | ## Dropbox Integration 25 | Integrating ``SimpleFileSync`` into your app involves the following steps: 26 | 27 | 1. Copy the `simplefilesync` folder into your project. 28 | Then add a gradle dependency to your app-level `build.gradle`: 29 | ```Groovy 30 | dependencies { 31 | .... 32 | implementation project(path: ':simplefilesync') 33 | } 34 | ``` 35 | Furthermore, add `simplefilesync` to your root `settings.gradle`: 36 | ```Groovy 37 | include ':app', ':simplefilesync' 38 | ``` 39 | 40 | 2. Add the following to your `AndroidManifest.xml`: 41 | ```XML 42 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ``` 54 | 55 | 3. Initiate the oauth2 flow at an appropriate place in your code, e.g. when the user clicks a button: 56 | ```Java 57 | DropboxFileSync.launchInitialOauthActivity(context); 58 | ``` 59 | 60 | 4. Insert the following call into the `onResume` of your activity: 61 | ```Java 62 | DropboxFileSync.onResumeFetchOAuthToken(this); 63 | ``` 64 | 65 | 5. To do the actual file synchronization, instantiate an instance of `DropboxFileSync` and execute it. 66 | A good place for this might be the `onResume` of your activity: 67 | ```Java 68 | new DropboxFileSync(this, myLocalFile, myRemoteFileName, 69 | new AbstractFileSync.SSyncCallback() { 70 | @Override 71 | public void onSyncCompleted(SSyncResult res) { 72 | // TODO: Check the result code and notify the user about success or failure. 73 | // TODO: If the result is DOWNLOAD_SUCCESS, then you will probably need to copy the temporary download file to a permanent storage location. 74 | } 75 | }).execute(); 76 | ``` 77 | 78 | ## Dropbox Production Integration 79 | 80 | For debugging purposes, this library includes an app key that is linked to a Dropbox sample app. 81 | For a production app, you need to register a Dropbox app according to the Dropbox developer documentation: https://www.dropbox.com/developers/. 82 | After registering your Dropbox app, add your app key to `strings.xml`. 83 | Replace the `MY_APP_KEY` entries with the app key from your Dropbox developer dashboard: 84 | ```XML 85 | MY_APP_KEY 86 | db-MY_APP_KEY 87 | ``` 88 | 89 | ## Potential Issues 90 | 91 | #### Optional Auto Backup Configuration 92 | It might be a good idea to disable the Android Auto Backup feature. 93 | This can be done by setting `android:allowBackup="false"` in your application `AndroidManifest.xml`. 94 | Alternatively, you might disable Auto Backup for a selected folder where your local sync files are stored. 95 | Otherwise, the Auto Backup might mess up the synchronization with Dropbox. 96 | 97 | 98 | #### Build Failure about META_INF/DEPENDENCIES 99 | If you get a message like `More than one file was found with OS independent path 'META-INF/DEPENDENCIES'`, 100 | then you should be able to fix it by adding the following entry to your app-level `build.gradle`: 101 | ``` 102 | android { 103 | .... 104 | packagingOptions { 105 | exclude 'META-INF/DEPENDENCIES' 106 | } 107 | } 108 | ``` 109 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/progress/ProgressMonitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.progress; 18 | 19 | import net.lingala.zip4j.exception.ZipException; 20 | 21 | /** 22 | * If Zip4j is set to run in thread mode, this class helps retrieve current progress 23 | * 24 | */ 25 | public class ProgressMonitor { 26 | 27 | private int state; 28 | private long totalWork; 29 | private long workCompleted; 30 | private int percentDone; 31 | private int currentOperation; 32 | private String fileName; 33 | private int result; 34 | private Throwable exception; 35 | private boolean cancelAllTasks; 36 | private boolean pause; 37 | 38 | //Progress monitor States 39 | public static final int STATE_READY = 0; 40 | public static final int STATE_BUSY = 1; 41 | 42 | //Progress monitor result codes 43 | public static final int RESULT_SUCCESS = 0; 44 | public static final int RESULT_WORKING = 1; 45 | public static final int RESULT_ERROR = 2; 46 | public static final int RESULT_CANCELLED = 3; 47 | 48 | //Operation Types 49 | public static final int OPERATION_NONE = -1; 50 | public static final int OPERATION_ADD = 0; 51 | public static final int OPERATION_EXTRACT = 1; 52 | public static final int OPERATION_REMOVE = 2; 53 | public static final int OPERATION_CALC_CRC = 3; 54 | public static final int OPERATION_MERGE = 4; 55 | 56 | public ProgressMonitor() { 57 | reset(); 58 | percentDone = 0; 59 | } 60 | 61 | public int getState() { 62 | return state; 63 | } 64 | 65 | public void setState(int state) { 66 | this.state = state; 67 | } 68 | 69 | public long getTotalWork() { 70 | return totalWork; 71 | } 72 | 73 | public void setTotalWork(long totalWork) { 74 | this.totalWork = totalWork; 75 | } 76 | 77 | public long getWorkCompleted() { 78 | return workCompleted; 79 | } 80 | 81 | public void updateWorkCompleted(long workCompleted) { 82 | this.workCompleted += workCompleted; 83 | 84 | if (totalWork > 0) { 85 | percentDone = (int)((this.workCompleted*100/totalWork)); 86 | if (percentDone > 100) { 87 | percentDone = 100; 88 | } 89 | } 90 | while (pause) { 91 | try { 92 | Thread.sleep(150); 93 | } catch (InterruptedException e) { 94 | //Do nothing 95 | } 96 | } 97 | } 98 | 99 | public int getPercentDone() { 100 | return percentDone; 101 | } 102 | 103 | public void setPercentDone(int percentDone) { 104 | this.percentDone = percentDone; 105 | } 106 | 107 | public int getResult() { 108 | return result; 109 | } 110 | 111 | public void setResult(int result) { 112 | this.result = result; 113 | } 114 | 115 | public String getFileName() { 116 | return fileName; 117 | } 118 | 119 | public void setFileName(String fileName) { 120 | this.fileName = fileName; 121 | } 122 | 123 | public int getCurrentOperation() { 124 | return currentOperation; 125 | } 126 | 127 | public void setCurrentOperation(int currentOperation) { 128 | this.currentOperation = currentOperation; 129 | } 130 | 131 | public Throwable getException() { 132 | return exception; 133 | } 134 | 135 | public void setException(Throwable exception) { 136 | this.exception = exception; 137 | } 138 | 139 | public void endProgressMonitorSuccess() throws ZipException { 140 | reset(); 141 | result = ProgressMonitor.RESULT_SUCCESS; 142 | } 143 | 144 | public void endProgressMonitorError(Throwable e) throws ZipException { 145 | reset(); 146 | result = ProgressMonitor.RESULT_ERROR; 147 | exception = e; 148 | } 149 | 150 | public void reset() { 151 | currentOperation = OPERATION_NONE; 152 | state = STATE_READY; 153 | fileName = null; 154 | totalWork = 0; 155 | workCompleted = 0; 156 | percentDone = 0; 157 | } 158 | 159 | public void fullReset() { 160 | reset(); 161 | exception = null; 162 | result = RESULT_SUCCESS; 163 | } 164 | 165 | public boolean isCancelAllTasks() { 166 | return cancelAllTasks; 167 | } 168 | 169 | public void cancelAllTasks() { 170 | this.cancelAllTasks = true; 171 | } 172 | 173 | public boolean isPause() { 174 | return pause; 175 | } 176 | 177 | public void setPause(boolean pause) { 178 | this.pause = pause; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /zip4j/src/main/java/net/lingala/zip4j/model/ZipModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Srikanth Reddy Lingala 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.lingala.zip4j.model; 18 | 19 | import java.util.List; 20 | 21 | public class ZipModel implements Cloneable { 22 | 23 | private List localFileHeaderList; 24 | 25 | private List dataDescriptorList; 26 | 27 | private ArchiveExtraDataRecord archiveExtraDataRecord; 28 | 29 | private CentralDirectory centralDirectory; 30 | 31 | private EndCentralDirRecord endCentralDirRecord; 32 | 33 | private Zip64EndCentralDirLocator zip64EndCentralDirLocator; 34 | 35 | private Zip64EndCentralDirRecord zip64EndCentralDirRecord; 36 | 37 | private boolean splitArchive; 38 | 39 | private long splitLength; 40 | 41 | private String zipFile; 42 | 43 | private boolean isZip64Format; 44 | 45 | private boolean isNestedZipFile; 46 | 47 | private long start; 48 | 49 | private long end; 50 | 51 | private String fileNameCharset; 52 | 53 | public ZipModel() { 54 | splitLength = -1; 55 | } 56 | 57 | public List getLocalFileHeaderList() { 58 | return localFileHeaderList; 59 | } 60 | 61 | public void setLocalFileHeaderList(List localFileHeaderList) { 62 | this.localFileHeaderList = localFileHeaderList; 63 | } 64 | 65 | public List getDataDescriptorList() { 66 | return dataDescriptorList; 67 | } 68 | 69 | public void setDataDescriptorList(List dataDescriptorList) { 70 | this.dataDescriptorList = dataDescriptorList; 71 | } 72 | 73 | public CentralDirectory getCentralDirectory() { 74 | return centralDirectory; 75 | } 76 | 77 | public void setCentralDirectory(CentralDirectory centralDirectory) { 78 | this.centralDirectory = centralDirectory; 79 | } 80 | 81 | public EndCentralDirRecord getEndCentralDirRecord() { 82 | return endCentralDirRecord; 83 | } 84 | 85 | public void setEndCentralDirRecord(EndCentralDirRecord endCentralDirRecord) { 86 | this.endCentralDirRecord = endCentralDirRecord; 87 | } 88 | 89 | public ArchiveExtraDataRecord getArchiveExtraDataRecord() { 90 | return archiveExtraDataRecord; 91 | } 92 | 93 | public void setArchiveExtraDataRecord( 94 | ArchiveExtraDataRecord archiveExtraDataRecord) { 95 | this.archiveExtraDataRecord = archiveExtraDataRecord; 96 | } 97 | 98 | public boolean isSplitArchive() { 99 | return splitArchive; 100 | } 101 | 102 | public void setSplitArchive(boolean splitArchive) { 103 | this.splitArchive = splitArchive; 104 | } 105 | 106 | public String getZipFile() { 107 | return zipFile; 108 | } 109 | 110 | public void setZipFile(String zipFile) { 111 | this.zipFile = zipFile; 112 | } 113 | 114 | public Zip64EndCentralDirLocator getZip64EndCentralDirLocator() { 115 | return zip64EndCentralDirLocator; 116 | } 117 | 118 | public void setZip64EndCentralDirLocator( 119 | Zip64EndCentralDirLocator zip64EndCentralDirLocator) { 120 | this.zip64EndCentralDirLocator = zip64EndCentralDirLocator; 121 | } 122 | 123 | public Zip64EndCentralDirRecord getZip64EndCentralDirRecord() { 124 | return zip64EndCentralDirRecord; 125 | } 126 | 127 | public void setZip64EndCentralDirRecord( 128 | Zip64EndCentralDirRecord zip64EndCentralDirRecord) { 129 | this.zip64EndCentralDirRecord = zip64EndCentralDirRecord; 130 | } 131 | 132 | public boolean isZip64Format() { 133 | return isZip64Format; 134 | } 135 | 136 | public void setZip64Format(boolean isZip64Format) { 137 | this.isZip64Format = isZip64Format; 138 | } 139 | 140 | public boolean isNestedZipFile() { 141 | return isNestedZipFile; 142 | } 143 | 144 | public void setNestedZipFile(boolean isNestedZipFile) { 145 | this.isNestedZipFile = isNestedZipFile; 146 | } 147 | 148 | public long getStart() { 149 | return start; 150 | } 151 | 152 | public void setStart(long start) { 153 | this.start = start; 154 | } 155 | 156 | public long getEnd() { 157 | return end; 158 | } 159 | 160 | public void setEnd(long end) { 161 | this.end = end; 162 | } 163 | 164 | public long getSplitLength() { 165 | return splitLength; 166 | } 167 | 168 | public void setSplitLength(long splitLength) { 169 | this.splitLength = splitLength; 170 | } 171 | 172 | public Object clone() throws CloneNotSupportedException { 173 | return super.clone(); 174 | } 175 | 176 | public String getFileNameCharset() { 177 | return fileNameCharset; 178 | } 179 | 180 | public void setFileNameCharset(String fileNameCharset) { 181 | this.fileNameCharset = fileNameCharset; 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/password/PwManager.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.password 2 | 3 | import android.os.Build 4 | import android.widget.Toast 5 | import androidx.fragment.app.FragmentActivity 6 | import com.ditronic.securezipnotes.dialogs.PwDialog 7 | import com.ditronic.securezipnotes.util.Boast 8 | import com.ditronic.securezipnotes.zip.CryptoZip 9 | import net.lingala.zip4j.io.ZipInputStream 10 | import net.lingala.zip4j.model.FileHeader 11 | import timber.log.Timber 12 | 13 | sealed class PwResult { 14 | class Success(val inputStream: ZipInputStream?, 15 | val password: String): PwResult() 16 | object Failure : PwResult() 17 | } 18 | 19 | class PwRequest( 20 | val fileHeader: FileHeader, 21 | val continuation: (res: PwResult.Success) -> Unit 22 | ) 23 | 24 | object PwManager { 25 | 26 | private var cachedPw: String? = null 27 | 28 | val cachedPassword 29 | get() = cachedPw 30 | 31 | 32 | private fun onRetrievedPassword(ac: FragmentActivity, pwRequest: PwRequest, pw: String?) { 33 | if (pw == null) { 34 | showPasswordDialog(ac, pwRequest = pwRequest) 35 | return 36 | } 37 | val zipStream = CryptoZip.instance(ac).isPasswordValid(pwRequest.fileHeader, pw) 38 | if (zipStream != null) { 39 | cachedPw = pw // Assign password before running any callback! 40 | pwRequest.continuation(PwResult.Success(inputStream = zipStream, password = pw)) // Password valid, run success callback. 41 | } else { 42 | Timber.d("Outdated password, invalidate preferences and show password dialog") 43 | cachedPw = null 44 | clearPrivatePrefs(ac) 45 | // Ask the user for the right password, which runs the callback later on. 46 | showPasswordDialog(ac, pwRequest = pwRequest) 47 | } 48 | } 49 | 50 | 51 | fun retrievePasswordAsync(ac: FragmentActivity, fileHeader: FileHeader, continuation: (res: PwResult.Success) -> Unit) { 52 | val pwRequest = PwRequest(fileHeader = fileHeader, continuation = continuation) 53 | val cachedPassword = cachedPassword 54 | if (cachedPassword != null) { 55 | onRetrievedPassword(ac, pwRequest = pwRequest, pw = cachedPassword) // Synchronous case: Password already present 56 | return 57 | } 58 | 59 | if (Build.VERSION.SDK_INT < 23) { 60 | val pw = getOldApiPw(ac) 61 | onRetrievedPassword(ac, pwRequest = pwRequest, pw = pw) // Synchronous case: Old API 62 | return 63 | } 64 | 65 | val cipherToUnlock = initDecryptCipher(ac) 66 | if (cipherToUnlock == null) { 67 | onRetrievedPassword(ac, pwRequest = pwRequest, pw = null) // Synchronous case: Cipher failure 68 | return 69 | } 70 | 71 | unlockCipherWithBiometricPrompt(ac, cipherToUnlock) { unlockedCipher -> 72 | val pw = if (unlockedCipher != null) { 73 | decryptPassword(ac, unlockedCipher) 74 | } else { 75 | null 76 | } 77 | onRetrievedPassword(ac, pwRequest = pwRequest, pw = pw) // Asynchronous case 78 | } 79 | } 80 | 81 | 82 | private fun showPasswordDialog(ac: FragmentActivity, pwRequest: PwRequest) { 83 | PwDialog().show(activity = ac, state = pwRequest) 84 | } 85 | 86 | private enum class SyncMode { 87 | SYNC, ASYNC 88 | } 89 | 90 | fun saveUserProvidedPassword(ac: FragmentActivity, res: PwResult.Success, cb: (res: PwResult.Success) -> Unit) { 91 | val syncMode = savePasswordInternal(ac, res, cb) 92 | if (syncMode == SyncMode.SYNC) { 93 | cb(res) 94 | } 95 | } 96 | 97 | 98 | private fun savePasswordInternal(ac: FragmentActivity, res: PwResult.Success, cb: (res: PwResult.Success) -> Unit): SyncMode { 99 | 100 | cachedPw = res.password // Assign password before running any callback! 101 | 102 | // The salt must be different for each file since this ZIP format uses counter mode with a constant IV! 103 | // Therefore we cannot simply store a key that is derived via PBKDF2. Instead, we encrypt the password via KeyStore. 104 | 105 | if (Build.VERSION.SDK_INT < 23) { 106 | saveLowAPIPw(res.password, ac) 107 | return SyncMode.SYNC 108 | } 109 | 110 | val secretKey = tryGenerateKeyStoreKey() ?: return SyncMode.SYNC 111 | val cipherToUnlock = initEncryptCipher(secretKey) ?: return SyncMode.SYNC 112 | 113 | // Unlock the freshly created key in order to encrypt the password. 114 | unlockCipherWithBiometricPrompt(ac, cipherToUnlock) { unlockedCipher -> 115 | val success = finalizePwEncryption(ac, res.password, unlockedCipher) 116 | if (success) { 117 | Boast.makeText(ac, "Password configured successfully", Toast.LENGTH_LONG).show() 118 | } else { 119 | Boast.makeText(ac, "Failed to encrypt the password", Toast.LENGTH_LONG).show() 120 | } 121 | cb(res) // Callback must be executed regardless of success or failure. 122 | } 123 | return SyncMode.ASYNC // Asynchronous case 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/src/main/java/com/ditronic/securezipnotes/zip/NotesImport.kt: -------------------------------------------------------------------------------- 1 | package com.ditronic.securezipnotes.zip 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import androidx.appcompat.app.AlertDialog 6 | import com.ditronic.simplefilesync.util.FilesUtil 7 | import net.lingala.zip4j.core.ZipFile 8 | import net.lingala.zip4j.model.FileHeader 9 | import net.lingala.zip4j.util.Zip4jConstants 10 | import timber.log.Timber 11 | import java.io.File 12 | import java.io.FileNotFoundException 13 | import java.io.IOException 14 | import java.io.InputStream 15 | 16 | 17 | object NotesImport { 18 | 19 | private val TAG = NotesImport::class.java.name 20 | 21 | private fun alertDialog(cx: Context, message: String) { 22 | AlertDialog.Builder(cx) 23 | .setMessage(message) 24 | .setPositiveButton(android.R.string.ok) { _, _ -> }.show() 25 | } 26 | 27 | private fun validateCompressionMethod(cx: Context, fileHeader: FileHeader) : Boolean { 28 | val aesExtraDataRecord = fileHeader.aesExtraDataRecord 29 | if (aesExtraDataRecord == null) { 30 | alertDialog(cx, "Import failed. Could not find AES data record.") 31 | return false 32 | } 33 | val compMethod = aesExtraDataRecord.compressionMethod 34 | if (compMethod == Zip4jConstants.COMP_STORE) { 35 | return true 36 | } else if (compMethod == Zip4jConstants.COMP_DEFLATE) { 37 | return true 38 | } else if (compMethod == 12) { 39 | alertDialog(cx, "Import failed: This app does not support BZIP2 compression.") 40 | return false 41 | } else if (compMethod == 9) { 42 | alertDialog(cx, "Import failed: This app does not support DEFLATE64 compression.") 43 | return false 44 | } else if (compMethod == 14) { 45 | alertDialog(cx, "Import failed: This app does not support LZMA compression.") 46 | return false 47 | } else if (compMethod == 98) { 48 | alertDialog(cx, "Import failed: This app does not support PPMD compression.") 49 | return false 50 | } else { 51 | alertDialog(cx, "Import failed: Unsupported compression method (" + compMethod + ").") 52 | return false 53 | } 54 | } 55 | 56 | private fun isValidAesZipFile(cx: Context, tmpFile: File): Boolean { 57 | val tmpZipFile: ZipFile 58 | try { 59 | tmpZipFile = ZipFile(tmpFile.path) 60 | tmpZipFile.readZipInfo() 61 | } catch (e: Exception) { 62 | Timber.e(e) 63 | alertDialog(cx, "Import failed. Probably this is not a valid Zip file.") 64 | return false 65 | } 66 | 67 | val fileHeaders = tmpZipFile.fileHeadersFast 68 | 69 | fileHeaders.forEach { 70 | if (it.isDirectory) { 71 | alertDialog(cx, "Import failed. Zip files with pure directory entries are not supported.") 72 | return false 73 | } 74 | } 75 | fileHeaders.forEach { 76 | if (!it.isEncrypted) { 77 | alertDialog(cx, "Import failed. Zip files with non-encrypted entries are not supported.") 78 | return false 79 | } 80 | } 81 | fileHeaders.forEach { 82 | if (it.encryptionMethod != Zip4jConstants.ENC_METHOD_AES) { 83 | alertDialog(cx, "Unsupported encryption algorithm. This app only supports Zip files with AES encryption.") 84 | return false 85 | } 86 | } 87 | fileHeaders.forEach { 88 | if (it.fileName.isEmpty()) { 89 | alertDialog(cx, "File names must not have zero lengths.") 90 | return false 91 | } 92 | } 93 | fileHeaders.forEach { 94 | if (!validateCompressionMethod(cx, it)) { 95 | return false 96 | } 97 | } 98 | return true 99 | } 100 | 101 | fun importFromFile(cx: Context, tmpFile: File, successMessage: String) { 102 | if (!isValidAesZipFile(cx, tmpFile)) { 103 | tmpFile.delete() 104 | return 105 | } 106 | 107 | try { 108 | FilesUtil.copyFile(tmpFile, CryptoZip.getMainFilePath(cx)) 109 | } catch (e: IOException) { 110 | throw RuntimeException(e) 111 | } 112 | 113 | tmpFile.delete() 114 | CryptoZip.resetCryptoZip(cx) // Refresh data after an import 115 | alertDialog(cx, successMessage) 116 | } 117 | 118 | fun importFromUri(cx: Context, importUri: Uri?) { 119 | if (importUri == null) { 120 | alertDialog(cx, "Failed to select a file") 121 | return 122 | } 123 | 124 | val `is`: InputStream? 125 | try { 126 | `is` = cx.contentResolver.openInputStream(importUri) 127 | } catch (e: FileNotFoundException) { 128 | alertDialog(cx, "Failed to find file.") 129 | return 130 | } 131 | 132 | if (`is` == null) { 133 | alertDialog(cx, "Failed to open file.") 134 | return 135 | } 136 | val tmpFile = FilesUtil.streamToTmpFile(`is`) 137 | importFromFile(cx, tmpFile, "Successfully imported zip notes.") 138 | } 139 | } 140 | --------------------------------------------------------------------------------