简介:这是一份面向Android应用开发初学者与信息安全实践者的Java语言文件加密器项目源码,聚焦移动端敏感文件加密需求,适用于课程设计、毕业设计及安全工具原型开发场景。资源共60个文件,包含18个Java核心逻辑文件(实现AES/DES/RSA等多算法加解密)、19个XML配置文件(定义UI布局与资源参数)、5个JPG与4个PNG界面截图及图标素材,以及3个Gradle构建脚本、2个Git忽略文件和项目说明文档等,整体压缩包仅967KB,轻量易导入。已有336人学习下载,资源结构清晰:app模块下含完整Activity流程与加密服务封装,gradle-wrapper与build.gradle确保一键构建,LICENSE与README.txt明确开源协议与使用指引,JavaApk源码说明.txt则详解各模块职责与算法调用路径,便于快速理解架构并二次开发。
1. 项目缘起:为什么我们需要一个Android文件加密器?
在移动办公和数字生活成为常态的今天,我们的手机里塞满了各种敏感文件:工作合同、个人证件照片、私密日记、财务记录等等。你可能遇到过这样的场景:手机借给朋友或家人用一下,心里却总有点不踏实,担心他们无意间翻到不该看的东西;或者手机不慎丢失,第一时间涌上心头的恐慌不是手机本身的价值,而是里面存储的隐私数据会流向何处。系统自带的“应用锁”或“私密空间”功能,往往只能保护特定应用,对于文件管理器里那些零散的文件,防护就显得力不从心。
这正是“基于Java语言的Android文件加密器”项目诞生的背景。它不是一个复杂的商业级安全套件,而是一个轻量、自主可控的工具,让你能亲手为手机里的任意文件加上一把“数字锁”。使用Java语言在Android平台上实现,意味着你可以完全理解其运作机制,从生成密钥、选择加密算法,到最终完成文件的加密与解密,每一步都清晰可见。对于Android开发者而言,这更是一个绝佳的练手项目,它能让你深入理解Android的文件系统(File I/O)、密码学API(如javax.crypto)的使用、以及如何在UI线程与后台任务之间安全高效地切换。接下来,我将带你从零开始,拆解这个项目的核心设计与实现,并分享我在开发过程中积累的实战经验和那些容易踩坑的细节。
2. 核心架构设计:如何构建一个健壮的加密工具?
一个文件加密器,远不止是调用一个加密函数那么简单。它需要一套完整的架构来保证安全性、易用性和性能。我们不能简单地在主线程里进行大文件的加密解密操作,那会直接导致应用无响应(ANR);我们也不能把加密密钥明文存储在SharedPreferences里,那等于把钥匙挂在门上。
2.1 分层架构与模块职责
我设计的架构主要分为三层,职责清晰,便于维护和扩展:
表示层(UI层):基于Android的
Activity/Fragment和ViewModel构建。负责展示文件列表、加密解密进度、处理用户交互(如选择文件、输入密码、启动操作)。这一层要尽可能“薄”,只做界面更新和事件转发。领域层(业务逻辑层):这是核心。我创建了一个
FileEncryptionManager单例类来统筹所有加密解密业务。它不关心界面,只接收来自UI层的命令(如“加密这个文件,密码是XXX”),然后协调数据层和工具层完成工作。这里也是处理业务规则的地方,比如判断文件是否已被加密、验证密码强度等。数据层与工具层:
- 数据层:负责文件的读写。使用Android的
ContentResolver来安全地访问用户通过文件选择器(如Intent.ACTION_OPEN_DOCUMENT)授予URI权限的文件,这是处理Android沙盒机制和Scoped Storage的关键。 - 工具层:包含真正的加密解密引擎
CryptoEngine,以及密钥派生工具KeyDerivation。它们是完全无状态的、纯Java的类,只负责算法实现。
- 数据层:负责文件的读写。使用Android的
这种分层的好处是,如果未来我想换一个UI框架(比如Compose),或者更换加密算法,只需要替换对应的层,其他部分几乎不用改动。
2.2 关键类的设计与交互
让我们看看几个核心类是如何协作的:
MainViewModel:持有UI状态(如文件列表、进度条数值)。它接收UI事件,调用FileEncryptionManager的方法,并将结果通过LiveData或StateFlow通知UI更新。FileEncryptionManager:业务中枢。它内部维护一个线程池(ExecutorService)来执行耗时的加密解密任务,避免阻塞UI。当接到任务时,它会先通过KeyDerivation根据用户密码生成密钥,然后调用CryptoEngine执行加密,并利用数据层的FileRepository读写文件,同时通过回调或Flow向ViewModel报告进度。CryptoEngine:这是密码学实现的核心。我选择了AES(高级加密标准)作为对称加密算法,因为它安全、高效且被广泛支持。具体模式上,我使用AES/GCM/NoPadding。GCM(Galois/Counter Mode)是一种认证加密模式,它不仅能保密数据,还能验证数据的完整性(防止被篡改),比传统的CBC模式更安全且通常更快。CryptoEngine提供两个静态方法:encrypt(InputStream, OutputStream, SecretKey)和decrypt(InputStream, OutputStream, SecretKey)。
注意:密钥管理是生命线。绝对不要直接使用用户输入的字符串作为AES密钥。我们必须使用基于密码的密钥派生函数(PBKDF),如
PBKDF2WithHmacSHA256。这个过程会通过加盐(Salt)和多次迭代(例如10万次),将简单的密码转化为符合长度要求的、抗暴力破解的加密密钥。盐值需要随机生成并与加密后的文件一起保存。
3. 关键技术实现细节与踩坑实录
有了架构蓝图,我们来深入代码层面,看看那些决定成败的细节。这里我会结合代码片段和踩坑经验,让你少走弯路。
3.1 安全地处理用户密码与密钥派生
这是整个系统安全性的基石。错误做法:byte[] key = password.getBytes(StandardCharsets.UTF_8);。正确做法如下:
public class KeyDerivation { private static final int ITERATION_COUNT = 100000; private static final int KEY_LENGTH = 256; // AES-256 private static final String ALGORITHM = "PBKDF2WithHmacSHA256"; public static SecretKey deriveKey(char[] password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException { PBEKeySpec spec = new PBEKeySpec(password, salt, ITERATION_COUNT, KEY_LENGTH); SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM); byte[] keyBytes = factory.generateSecret(spec).getEncoded(); // 清除密码字符数组的敏感数据 spec.clearPassword(); return new SecretKeySpec(keyBytes, "AES"); } public static byte[] generateSalt() { SecureRandom random = new SecureRandom(); byte[] salt = new byte[16]; random.nextBytes(salt); return salt; } }踩坑点1:使用char[]而非String存储密码。String在Java中是不可变的,会长时间驻留在内存中,直到被垃圾回收,有被内存转储攻击的风险。而char[]在使用后可以手动覆盖(如用Arrays.fill(password, '\0')),更安全。这也是为什么JPasswordField返回的是char[]。
踩坑点2:迭代次数与性能平衡。迭代次数太少(如1000次),密钥容易受到暴力破解。次数太多(如1000万次),在低端设备上会导致明显的卡顿。10万次是一个在安全性和用户体验之间比较平衡的常见值。你可以在首次使用时让用户选择“安全模式”(更高迭代次数)或“速度模式”。
3.2 实现AES-GCM加密解密引擎
CryptoEngine类的实现需要格外小心,GCM模式要求一个初始化向量(IV)和认证标签(Authentication Tag)。
public class CryptoEngine { private static final String TRANSFORMATION = "AES/GCM/NoPadding"; private static final int GCM_TAG_LENGTH = 128; // bits private static final int GCM_IV_LENGTH = 12; // bytes, 推荐值,性能好 public static void encrypt(InputStream inputStream, OutputStream outputStream, SecretKey secretKey) throws Exception { Cipher cipher = Cipher.getInstance(TRANSFORMATION); byte[] iv = new byte[GCM_IV_LENGTH]; SecureRandom.getInstanceStrong().nextBytes(iv); // 生成随机IV GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec); // 首先将IV写入输出文件头部!解密时需要它。 outputStream.write(iv); try (CipherOutputStream cos = new CipherOutputStream(outputStream, cipher)) { byte[] buffer = new byte[8192]; // 8KB缓冲区 int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { cos.write(buffer, 0, bytesRead); } } // CipherOutputStream关闭时会自动计算并追加GCM认证标签 } public static boolean decrypt(InputStream inputStream, OutputStream outputStream, SecretKey secretKey) throws Exception { // 先从文件头部读取IV byte[] iv = new byte[GCM_IV_LENGTH]; int bytesRead = inputStream.read(iv); if (bytesRead != GCM_IV_LENGTH) { throw new IllegalArgumentException("Invalid encrypted file format: IV missing or corrupted"); } Cipher cipher = Cipher.getInstance(TRANSFORMATION); GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec); try (CipherInputStream cis = new CipherInputStream(inputStream, cipher)) { byte[] buffer = new byte[8192]; while ((bytesRead = cis.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } } catch (AEADBadTagException e) { // 认证失败!密码错误或文件被篡改。 return false; } return true; } }踩坑点3:IV的管理。IV不需要保密,但必须唯一且不可预测。对于同一个密钥,绝对不要重复使用IV,否则会严重破坏GCM模式的安全性。所以每次加密都必须生成新的随机IV,并将其与密文一起存储(通常放在文件开头)。解密时先读取IV。
踩坑点4:处理认证失败。GCM解密时,如果密码错误或密文被篡改,CipherInputStream在读取时会抛出AEADBadTagException。我们必须捕获这个异常,并给用户返回“密码错误或文件已损坏”的友好提示,而不是让应用崩溃。这是GCM相比CBC模式的一个巨大优势——它能告诉你解密是否真正成功。
3.3 征服Android文件系统与后台任务
在Android上操作文件,尤其是大文件,是另一个挑战。
文件访问:从Android 10(API 29)开始,Scoped Storage成为强制要求。我们不能直接使用FileAPI访问任意路径。推荐使用Intent.ACTION_OPEN_DOCUMENT和Intent.ACTION_CREATE_DOCUMENT来让用户选择文件并授予URI权限。使用ContentResolver的openInputStream(Uri)和openOutputStream(Uri)来读写。
后台任务:加密一个1GB的视频文件可能需要几十秒。我们必须使用后台线程。AsyncTask已过时,推荐使用Kotlin协程+ViewModel,或者RxJava,或者纯粹的ExecutorService。在FileEncryptionManager中,我这样处理:
public class FileEncryptionManager { private final ExecutorService executor = Executors.newFixedThreadPool(2); // 限制并发数 private final MutableLiveData<ProgressState> progressLiveData = new MutableLiveData<>(); public void encryptFile(@NonNull Context context, @NonNull Uri sourceUri, @NonNull Uri destUri, @NonNull char[] password) { executor.submit(() -> { // 更新状态:开始 progressLiveData.postValue(new ProgressState(Status.RUNNING, 0, “正在准备...”)); try (InputStream is = context.getContentResolver().openInputStream(sourceUri); OutputStream os = context.getContentResolver().openOutputStream(destUri)) { byte[] salt = KeyDerivation.generateSalt(); // 将盐值写入目标文件头部(在IV之前) os.write(salt); SecretKey key = KeyDerivation.deriveKey(password, salt); // 此处需要实现一个能报告进度的包装流(如重写write方法计算百分比) ProgressReportingInputStream pis = new ProgressReportingInputStream(is, totalFileSize -> { // 计算并更新进度 int progress = (int) ((totalFileSize * 100) / fileLength); progressLiveData.postValue(new ProgressState(Status.RUNNING, progress, “加密中...”)); }); CryptoEngine.encrypt(pis, os, key); progressLiveData.postValue(new ProgressState(Status.SUCCESS, 100, “加密完成”)); } catch (Exception e) { progressLiveData.postValue(new ProgressState(Status.FAILED, 0, “加密失败: ” + e.getMessage())); } }); } // ... 解密方法类似 }踩坑点5:进度更新的精度与性能。如果每读取一个字节就更新一次进度,UI会频繁刷新,极其消耗性能。我的做法是,在ProgressReportingInputStream里,每读取一定数据量(例如64KB)或每隔一定时间(如200毫秒)才计算并回调一次进度。同时,进度回调必须通过postValue切换到主线程更新UI。
踩坑点6:妥善处理生命周期。如果用户在加密过程中退出应用或旋转屏幕,任务应该被妥善取消,避免内存泄漏和无效操作。在ViewModel的onCleared()方法中,需要调用executor.shutdownNow()来尝试中断所有任务。
4. 项目扩展与进阶优化思路
一个基础的文件加密器完成后,我们可以从多个维度对其进行增强,使其更实用、更专业。
4.1 功能扩展:从工具到解决方案
- 批量操作与队列管理:允许用户选择一个文件夹进行批量加密/解密。实现一个任务队列,用户可以暂停、继续或取消队列中的任务。这涉及到更复杂的状态管理。
- 多种加密算法支持:除了AES-256-GCM,可以集成
ChaCha20-Poly1305(在某些平台上可能比AES更快)作为可选算法。在UI上让用户选择,并在文件头用一个魔数(Magic Number)标识算法。 - 云存储集成加密:开发一个“安全上传”功能,在文件上传到云盘(如集成Google Drive API)之前,先进行本地加密。这样,即使云服务提供商也无法查看你的文件内容。
- 图片/视频的缩略图保护:Android系统会自动为媒体文件生成缩略图,这可能导致信息泄露。可以探索在加密后,如何清理或加密这些缓存缩略图。
4.2 性能与体验优化
- Native加速:对于超大型文件,纯Java的加密速度可能成为瓶颈。可以考虑使用Android NDK,调用OpenSSL库的C/C++实现来进行加密解密,能获得显著的性能提升。这需要建立JNI桥接。
- 智能内存管理:对于极端大的文件(比如4GB以上),需要确保流式处理(Streaming)始终如一,避免试图将整个文件读入内存。我们的
CipherInputStream和缓冲区方案已经做到了这一点,但需要反复测试验证。 - 后台服务与通知:将长时间任务移至
ForegroundService,并显示持续的通知,即使用户切换了应用,任务也能继续执行。通知里可以显示进度和取消按钮。
4.3 安全性加固
- 密钥库(Android Keystore)集成:目前我们的密钥是由密码派生的,并可能短暂存在于内存中。对于更高安全要求的场景,可以使用Android Keystore系统来生成和存储非对称密钥对(RSA)。加密时,使用随机生成的AES文件密钥加密数据,再用Keystore中的RSA公钥加密这个AES密钥。解密时,用Keystore中的私钥(硬件保护)先解密出AES密钥。这样即使手机被root,AES密钥也受到硬件安全模块(如果有)的保护。
- 防止侧信道攻击:确保代码中没有基于加密时间的分支操作(时间侧信道)。使用恒定时间的函数比较密码哈希(如
MessageDigest.isEqual)。虽然对于移动应用来说威胁模型不一定包含这种高级攻击,但作为最佳实践值得了解。 - 模糊处理与反调试:如果发布正式应用,可以对代码进行混淆(ProGuard/R8),增加逆向工程的难度。还可以加入运行时检测,如果发现调试器附着,则清除内存中的密钥并退出。
开发这个项目的过程,让我对Android安全编程的理解深入了一个层次。它不仅仅是调用几个API,更是对资源生命周期、并发处理、用户体验和安全边界之间不断权衡的艺术。最深刻的体会是,安全是一个过程,而不是一个特性。从密码输入框的类型(TextPasswordvsTextVisiblePassword),到内存中密钥的存留时间,再到异常处理时是否泄露了栈信息,每一个细节都可能成为防线上的缺口。
本文还有配套的精品资源,点击获取