Skip to content

Commit 1b193dd

Browse files
feat: Add Android mobile app with cross-platform crypto compatibility
- Android native app (Kotlin + Jetpack Compose + Material 3) - AES-CBC + PBKDF2-HMAC-SHA256 crypto engine (javax.crypto) - Byte-identical output to Python desktop version - MVVM + Clean Architecture with Hilt DI - Responsive UI: three-column on tablets, TabRow on phones - Chinese/English i18n via Android resources - Settings persistence via DataStore - Unit tests: CryptoManager (25 test cases) + ViewModels - GitHub Actions CI: lint -> test -> build APK artifact - Cross-platform test vectors (test_vectors.json) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 91e2d5c commit 1b193dd

50 files changed

Lines changed: 3500 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Android Build
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
paths:
7+
- 'android/**'
8+
- '.github/workflows/android-build.yml'
9+
pull_request:
10+
branches: [main, master]
11+
paths:
12+
- 'android/**'
13+
- '.github/workflows/android-build.yml'
14+
workflow_dispatch:
15+
16+
jobs:
17+
lint-and-test:
18+
name: Lint & Unit Tests
19+
runs-on: ubuntu-latest
20+
defaults:
21+
run:
22+
working-directory: android
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v4
26+
27+
- name: Set up JDK 17
28+
uses: actions/setup-java@v4
29+
with:
30+
distribution: temurin
31+
java-version: '17'
32+
33+
- name: Setup Gradle
34+
uses: gradle/actions/setup-gradle@v4
35+
with:
36+
gradle-version: 8.7
37+
38+
- name: Run unit tests
39+
run: gradle testDebugUnitTest
40+
41+
- name: Upload test reports
42+
uses: actions/upload-artifact@v4
43+
if: failure()
44+
with:
45+
name: test-reports
46+
path: android/app/build/reports/tests/
47+
retention-days: 7
48+
49+
build-apk:
50+
name: Build Debug APK
51+
runs-on: ubuntu-latest
52+
needs: lint-and-test
53+
defaults:
54+
run:
55+
working-directory: android
56+
steps:
57+
- name: Checkout
58+
uses: actions/checkout@v4
59+
60+
- name: Set up JDK 17
61+
uses: actions/setup-java@v4
62+
with:
63+
distribution: temurin
64+
java-version: '17'
65+
66+
- name: Setup Gradle
67+
uses: gradle/actions/setup-gradle@v4
68+
with:
69+
gradle-version: 8.7
70+
71+
- name: Assemble debug APK
72+
run: gradle assembleDebug
73+
74+
- name: Upload APK artifact
75+
uses: actions/upload-artifact@v4
76+
with:
77+
name: app-debug
78+
path: android/app/build/outputs/apk/debug/app-debug.apk
79+
retention-days: 30

.gitignore

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,32 @@
22
.venv/
33
venv/
44
env/
5+
6+
# Python
7+
__pycache__/
8+
*.pyc
9+
*.pyo
10+
11+
# IDE
12+
.idea/
13+
*.iml
14+
15+
# Logs
16+
*.log
17+
18+
# Build outputs
19+
*.spec
20+
build/
21+
dist/
22+
23+
# Android
24+
android/.gradle/
25+
android/build/
26+
android/app/build/
27+
android/local.properties
28+
android/captures/
29+
android/.externalNativeBuild/
30+
31+
# OS
32+
.DS_Store
33+
Thumbs.db

CLAUDE.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
A Python GUI encryption/decryption tool (Tkinter) using AES-CBC with PBKDF2-HMAC-SHA256 key derivation. Supports text and file operations with drag-and-drop, progress tracking, and Chinese/English i18n.
8+
9+
## Commands
10+
11+
```bash
12+
# Run the application
13+
python main.py
14+
15+
# Install dependencies
16+
pip install -r requirements.txt
17+
18+
# Build standalone executable with PyInstaller
19+
pyinstaller main.spec
20+
```
21+
22+
No test suite or linter is configured in this project.
23+
24+
## Architecture
25+
26+
### `main.py` — GUI layer (EncryptApp)
27+
28+
Single-class Tkinter application inheriting from either `TkinterDnD.Tk` (if `tkinterdnd2` is installed) or `tk.Tk` (fallback). Key design points:
29+
30+
- **i18n**: All UI strings live in the `TRANSLATIONS` dict at module level, keyed by semantic name (e.g. `"missing_pwd_msg"`) with `en`/`zh` values. `EncryptApp.tr(key)` resolves a key to the current language. Widgets are registered via `register_widget(widget, key, attr)` and batch-updated by `update_language()`. The language combobox triggers `update_language` on `<<ComboboxSelected>>`.
31+
- **Settings persistence**: `settings.json` is loaded on startup (`load_settings`) and written whenever paths, language, or iterations change (`save_settings`). Four keys: `enc_output_path`, `dec_output_path`, `language`, `iterations`.
32+
- **File operations run on daemon threads** (`_run_file_op``threading.Thread`). UI updates are posted back via `self.after(0, ...)`. Progress callbacks update `ttk.Progressbar` values as percentages.
33+
- **MAX_ITERATIONS** is 100,000,000, enforced in `check_iterations()`.
34+
35+
### `crypto_manager.py` — Crypto logic (CryptoManager)
36+
37+
Stateless crypto operations class (no constructor logic). Key constants: `SALT_SIZE=16`, `IV_SIZE=16`, `CHUNK_SIZE=64KB`, `ITERATIONS=10000` (overridable at runtime via the GUI — the app sets `crypto.ITERATIONS` from user input).
38+
39+
- **`_derive_key(password, salt, key_length)`**: PBKDF2HMAC with SHA256. Iterations come from `self.ITERATIONS`.
40+
- **Text encrypt/decrypt**: Encodes text as UTF-8, applies PKCS7 padding, encrypts with AES-CBC. Output format: `base64(salt + iv + ciphertext)`. No authentication tag (no HMAC/GCM) — decryption failures surface as padding errors or garbage output.
41+
- **File encrypt/decrypt**: Streams data in 64KB chunks. Writes `[salt][iv][encrypted_stream]` to output. Uses PKCS7 padding — for encryption the padder accumulates chunks and finalizes at EOF; for decryption the unpadder strips padding at EOF.
42+
- Algorithm mapping: `{"AES-128": 16, "AES-192": 24, "AES-256": 32}` (key lengths in bytes).
43+
44+
### `hook-tkinterdnd2.py` — PyInstaller hook
45+
46+
Collects `tkinterdnd2` data files for PyInstaller bundling. Referenced by `main.spec` via `hookspath=['.']`.
47+
48+
### `main.spec` — PyInstaller spec
49+
50+
Builds a windowed (no console) single-directory bundle with `encryption.ico` as the icon. Entry point is `main.py`.
51+
52+
### Android App (`android/`)
53+
54+
Kotlin + Jetpack Compose native Android port (API 26+). Same crypto format as the Python version — ciphertext produced by either platform can be decrypted by the other.
55+
56+
```bash
57+
# Run unit tests
58+
cd android && ./gradlew testDebugUnitTest
59+
60+
# Build debug APK
61+
cd android && ./gradlew assembleDebug
62+
# Output: android/app/build/outputs/apk/debug/app-debug.apk
63+
```
64+
65+
**Architecture (MVVM + Clean Architecture):**
66+
67+
- `core/crypto/CryptoManager.kt` — Pure Kotlin crypto engine using javax.crypto (JCA). Produces byte-identical output to `crypto_manager.py`. Uses `"AES/CBC/PKCS5Padding"` (JVM PKCS5 = PKCS7 for 16-byte blocks) and `PBKDF2WithHmacSHA256`.
68+
- `core/data/SettingsRepository.kt` — Jetpack DataStore persistence, replaces `settings.json`.
69+
- `core/di/AppModule.kt` — Hilt DI module providing singletons.
70+
- `ui/viewmodel/` — StateFlow-based ViewModels managing UI state via UDF pattern. Crypto operations run on `Dispatchers.IO`.
71+
- `ui/screens/` — Compose screens with responsive layout: three-column on wide (>=840dp), TabRow on narrow.
72+
- `ui/components/` — Reusable Composables: PasswordField, AlgorithmDropdown, IterationsField, ResultLog, FilePickerCard, OutputDirSelector.
73+
74+
**i18n**: Android resource-based (`res/values/strings.xml` + `res/values-zh/strings.xml`), runtime switching via `AppCompatDelegate.setApplicationLocales()`.
75+
76+
**CI/CD**: `.github/workflows/android-build.yml` — triggers on push to `main`/`master` when `android/**` changes. Runs unit tests → builds debug APK → uploads as artifact. Uses `gradle/actions/setup-gradle@v4` with Gradle 8.7.
77+
78+
**Gradle wrapper**: For local builds, install Android Studio (auto-generates wrapper) or run `gradle wrapper` in the `android/` directory. CI uses `gradle-version` parameter and does not require the wrapper jar.
79+
80+
**Cross-platform test vectors**: `test_vectors.json` (generated by Python) is consumed by `CryptoManagerTest.kt` to verify byte-level compatibility.
81+
82+
## i18n Pattern
83+
84+
To add a new translatable string:
85+
1. Add entries to `TRANSLATIONS` dict in `main.py`.
86+
2. For static labels: call `self.register_widget(widget, "key_name")` after creating the widget.
87+
3. For dynamic strings (message boxes, status labels): call `self.tr("key_name")` at the point of use.

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,46 @@ python main.py
7676
- **File Format**:
7777
- `[Salt (16 bytes)] [IV (16 bytes)] [Encrypted Data ...]`
7878

79+
## Android App
80+
81+
An Android mobile version is available in the `android/` directory.
82+
83+
### Features
84+
- Same AES-CBC + PBKDF2 crypto as the desktop version — cross-platform compatible
85+
- Jetpack Compose UI with Material 3 design
86+
- Responsive layout: three-column on tablets, tab-based on phones
87+
- Chinese/English language switching
88+
- Settings persistence via DataStore
89+
90+
### Build & Run
91+
92+
**Option 1: Android Studio** (recommended)
93+
1. Open the `android/` directory in Android Studio
94+
2. Wait for Gradle sync to complete
95+
3. Click **Run** to deploy to a connected device or emulator
96+
97+
**Option 2: Command Line**
98+
```bash
99+
# First, generate the Gradle wrapper (requires Gradle installed):
100+
cd android
101+
gradle wrapper
102+
103+
# Or open the project in Android Studio which auto-generates the wrapper.
104+
# Then build:
105+
cd android
106+
./gradlew assembleDebug
107+
# APK output: android/app/build/outputs/apk/debug/app-debug.apk
108+
```
109+
110+
**Run unit tests:**
111+
```bash
112+
cd android
113+
./gradlew testDebugUnitTest
114+
```
115+
116+
### CI/CD
117+
GitHub Actions (`.github/workflows/android-build.yml`) automatically builds and tests the Android app on every push. The debug APK is uploaded as an artifact.
118+
79119
# ⚠️ 免责声明
80120

81121
本仓库包含的加密/解密程序仅为**学习、研究和实验目的**而提供。作者不保证其安全性、完整性或适用性,使用本程序所产生的一切风险及后果由使用者自行承担。

android/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
*.iml
2+
.gradle
3+
/local.properties
4+
/.idea
5+
.DS_Store
6+
/build
7+
/captures
8+
.externalNativeBuild
9+
.cxx
10+
local.properties

android/app/build.gradle.kts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
plugins {
2+
alias(libs.plugins.android.application)
3+
alias(libs.plugins.kotlin.android)
4+
alias(libs.plugins.kotlin.compose)
5+
alias(libs.plugins.hilt.android)
6+
alias(libs.plugins.ksp)
7+
}
8+
9+
android {
10+
namespace = "com.example.encryptapp"
11+
compileSdk = 34
12+
13+
defaultConfig {
14+
applicationId = "com.example.encryptapp"
15+
minSdk = 26
16+
targetSdk = 34
17+
versionCode = 1
18+
versionName = "1.0.0"
19+
20+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
21+
}
22+
23+
buildTypes {
24+
release {
25+
isMinifyEnabled = true
26+
proguardFiles(
27+
getDefaultProguardFile("proguard-android-optimize.txt"),
28+
"proguard-rules.pro"
29+
)
30+
}
31+
debug {
32+
isMinifyEnabled = false
33+
}
34+
}
35+
36+
compileOptions {
37+
sourceCompatibility = JavaVersion.VERSION_17
38+
targetCompatibility = JavaVersion.VERSION_17
39+
}
40+
41+
kotlinOptions {
42+
jvmTarget = "17"
43+
}
44+
45+
buildFeatures {
46+
compose = true
47+
}
48+
49+
packaging {
50+
resources {
51+
excludes += "/META-INF/{AL2.0,LGPL2.1}"
52+
}
53+
}
54+
}
55+
56+
dependencies {
57+
// Compose BOM
58+
val composeBom = platform(libs.compose.bom)
59+
implementation(composeBom)
60+
implementation(libs.compose.ui)
61+
implementation(libs.compose.ui.graphics)
62+
implementation(libs.compose.ui.tooling.preview)
63+
implementation(libs.compose.material3)
64+
implementation(libs.compose.material.icons)
65+
debugImplementation(libs.compose.ui.tooling)
66+
67+
// Core
68+
implementation(libs.core.ktx)
69+
implementation(libs.activity.compose)
70+
71+
// Lifecycle
72+
implementation(libs.lifecycle.runtime.compose)
73+
implementation(libs.lifecycle.viewmodel.compose)
74+
75+
// Navigation
76+
implementation(libs.navigation.compose)
77+
78+
// Hilt DI
79+
implementation(libs.hilt.android)
80+
ksp(libs.hilt.compiler)
81+
implementation(libs.hilt.navigation.compose)
82+
83+
// DataStore
84+
implementation(libs.datastore.preferences)
85+
86+
// Testing
87+
testImplementation(libs.junit.jupiter)
88+
testImplementation(libs.mockk)
89+
testImplementation(libs.turbine)
90+
testImplementation(libs.coroutines.test)
91+
}
92+
93+
tasks.withType<Test> {
94+
useJUnitPlatform()
95+
}

android/app/proguard-rules.pro

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# javax.crypto classes are in java.* and are not obfuscated.
2+
# Keep Hilt generated classes
3+
-keep class dagger.hilt.** { *; }
4+
-keep class javax.inject.** { *; }
5+
-keep class * extends dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper { *; }
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
5+
android:maxSdkVersion="32" />
6+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
7+
android:maxSdkVersion="29" />
8+
9+
<application
10+
android:name=".EncryptApplication"
11+
android:allowBackup="true"
12+
android:icon="@mipmap/ic_launcher"
13+
android:label="@string/app_name"
14+
android:roundIcon="@mipmap/ic_launcher_round"
15+
android:supportsRtl="true"
16+
android:theme="@style/Theme.EncryptApp">
17+
18+
<activity
19+
android:name=".MainActivity"
20+
android:exported="true"
21+
android:windowSoftInputMode="adjustResize"
22+
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">
23+
<intent-filter>
24+
<action android:name="android.intent.action.MAIN" />
25+
<category android:name="android.intent.category.LAUNCHER" />
26+
</intent-filter>
27+
</activity>
28+
</application>
29+
</manifest>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.example.encryptapp
2+
3+
import android.app.Application
4+
import dagger.hilt.android.HiltAndroidApp
5+
6+
@HiltAndroidApp
7+
class EncryptApplication : Application()

0 commit comments

Comments
 (0)