Flutter大文件分段解密报错:无效填充块,求高效加载方案
问题:大AES加密音频文件的流式解密与快速加载(块填充错误)
我有一个AES加密的音频文件,密钥(Key)和初始化向量(IV)存储在sqflite数据库中,可通过DownloadedAudioModel获取,最终需要将解密后的音频加载到audioPlayer中播放。
目前全量解密的代码可以正常运行,但加载10MB的文件耗时过长——推测是因为一次性将所有加密字节读入内存后再解密导致的:
static Future<void> decryptAndLoadFile( DownloadedAudioModel downloadedAudioModel) async { String password = downloadedAudioModel.key ?? ""; final ivBase64 = downloadedAudioModel.encIV ?? ""; final directory = await getDownloadsDirectory(); final downloadDirectory = Directory(directory!.path); final encryptedFile = File( '${downloadDirectory.path}/songs/${downloadedAudioModel.songPath}.encrypted'); final encryptedBytes = await encryptedFile.readAsBytes(); final key = enc.Key.fromUtf8(password); List<int> iv = base64Decode(ivBase64).toList(); final encrypter = enc.Encrypter(enc.AES(key)); final decryptedBytes = encrypter.decryptBytes(enc.Encrypted(encryptedBytes), iv: enc.IV(Uint8List.fromList(iv))); await MyGlobals.audioPlayer.setAudioSource(AudioSource.uri( Uri.dataFromBytes(decryptedBytes), tag: MediaItem( id: downloadedAudioModel.id.toString(), title: downloadedAudioModel.songName!, artist: downloadedAudioModel.authorName, artUri: Uri.file( "${downloadDirectory.path}/avatars/${downloadedAudioModel.songAvatar}")))); }
为了实现更快加载,我尝试通过流分段读取加密字节并解密,但两次尝试都抛出ArgumentError (Invalid argument(s): Invalid or corrupted pad block)异常:
- 直接使用
openRead()分块读取并解密:
final encryptedFile = File( '${downloadDirectory.path}/songs/${downloadedAudioModel.songPath}.encrypted'); encryptedFile.openRead().forEach((element) { final decryptedBytes = encrypter.decryptBytes( enc.Encrypted(Uint8List.fromList(element)), iv: enc.IV(Uint8List.fromList(iv))); });
- 手动循环分块解密并返回流:
static Stream<List<int>> decryptFile( DownloadedAudioModel downloadedAudioModel, Directory downloadDirectory) async* { String password = downloadedAudioModel.key ?? ""; final ivBase64 = downloadedAudioModel.encIV ?? ""; final encryptedFile = File( '${downloadDirectory.path}/songs/${downloadedAudioModel.songPath}.encrypted'); final encryptedBytes = await encryptedFile.readAsBytes(); final key = enc.Key.fromUtf8(password); List<int> iv = base64Decode(ivBase64).toList(); final encrypter = enc.Encrypter(enc.AES(key)); try { int i = 0; while (i < encryptedBytes.length) { final chunk = encryptedBytes.sublist( i, math.min(i + 4096, encryptedBytes.length)); final decryptedChunk = _decryptChunk(encrypter, enc.IV(Uint8List.fromList(iv)), chunk); yield decryptedChunk; i += 4096; } } catch (e) { if (e is FormatException && e.message.contains('Invalid or corrupted pad block')) { yield* Stream.error( Exception( 'Error decrypting file: The provided password is incorrect or the file is corrupted.'), ); } else { yield* Stream.error(e); } } } static List<int> _decryptChunk( enc.Encrypter encrypter, enc.IV iv, List<int> encryptedChunk) { final encrypted = enc.Encrypted(Uint8List.fromList(encryptedChunk)); try { final decryptedChunk = encrypter.decryptBytes(encrypted, iv: iv); return decryptedChunk; } catch (e) { return []; } } static Future<void> playDecryptedAudio( DownloadedAudioModel downloadedAudioModel) async { final directory = await getDownloadsDirectory(); final downloadDirectory = Directory(directory!.path); try { final decryptedStream = decryptFile(downloadedAudioModel, downloadDirectory); final decryptedBytes = await decryptedStream.fold<List<int>>( [], (previousValue, element) => [...previousValue, ...element], ); await MyGlobals.audioPlayer.setAudioSource(AudioSource.uri( Uri.dataFromBytes(decryptedBytes), tag: MediaItem( id: downloadedAudioModel.id.toString(), title: downloadedAudioModel.songName!, artist: downloadedAudioModel.authorName, artUri: Uri.file( "${downloadDirectory.path}/avatars/${downloadedAudioModel.songAvatar}")))); await MyGlobals.audioPlayer.play(); } catch (e) { print('Error playing decrypted audio: $e'); } }
请问如何正确实现大文件的流式解密与快速加载?
解决方案
错误原因分析
流式解密失败的核心问题有两个:
- AES分组模式的状态丢失:如果使用CBC模式(AES默认常用模式),每个密文块的解密依赖前一个密文块作为IV,不能全程复用初始IV。
- 块大小与填充不匹配:AES是分组加密算法,块大小固定为16字节,加密后的总长度是16的整数倍(PKCS7填充)。非16倍数的分块或错误的填充处理会触发校验异常。
正确实现步骤
- 流式读取+维护解密状态:通过文件流逐块读取,分块大小设为16的整数倍(比如4096),每解密一块后更新IV为当前密文块的最后16字节。
- 直接流式加载到audioPlayer:使用
AudioSource.stream替代Uri.dataFromBytes,实现边解密边播放,无需将整个文件加载到内存。 - 单独处理最后一块的填充:最后一块解密后自动移除PKCS7填充,避免数据冗余。
完整代码示例
import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:just_audio/just_audio.dart'; import 'package:path_provider/path_provider.dart'; import 'package:encrypt/encrypt.dart' as enc; // 流式解密函数 static Stream<List<int>> decryptFileStream( DownloadedAudioModel downloadedAudioModel) async* { final key = enc.Key.fromUtf8(downloadedAudioModel.key ?? ""); final initialIvBytes = base64Decode(downloadedAudioModel.encIV ?? ""); final encrypter = enc.Encrypter(enc.AES(key, mode: enc.AESMode.cbc)); final directory = await getDownloadsDirectory(); final encryptedFile = File( '${directory!.path}/songs/${downloadedAudioModel.songPath}.encrypted'); const chunkSize = 4096; // 必须是16的倍数(4096/16=256) final fileLength = await encryptedFile.length(); var currentIvBytes = initialIvBytes; var remainingBytes = fileLength; final stream = encryptedFile.openRead(); await for (final chunk in stream) { remainingBytes -= chunk.length; Uint8List encryptedChunk = Uint8List.fromList(chunk); // 处理最后一块:完整解密并自动移除PKCS7填充 if (remainingBytes == 0) { final decrypted = encrypter.decryptBytes( enc.Encrypted(encryptedChunk), iv: enc.IV(currentIvBytes), ); yield decrypted; } else { // 中间块:只解密不处理填充,更新IV为当前密文块的最后16字节 final decrypted = encrypter.decryptBytes( enc.Encrypted(encryptedChunk), iv: enc.IV(currentIvBytes), padding: null, ); currentIvBytes = encryptedChunk.sublist(encryptedChunk.length - 16); yield decrypted; } } } // 播放解密后的音频 static Future<void> playDecryptedAudio( DownloadedAudioModel downloadedAudioModel) async { final directory = await getDownloadsDirectory(); try { final decryptedStream = decryptFileStream(downloadedAudioModel); // 直接用流加载音频,避免内存堆积 await MyGlobals.audioPlayer.setAudioSource( AudioSource.stream( decryptedStream, tag: MediaItem( id: downloadedAudioModel.id.toString(), title: downloadedAudioModel.songName!, artist: downloadedAudioModel.authorName, artUri: Uri.file( "${directory!.path}/avatars/${downloadedAudioModel.songAvatar}"), ), ), ); await MyGlobals.audioPlayer.play(); } catch (e) { print('Error playing decrypted audio: $e'); } }
关键注意事项
- 加密模式一致性:确保加密时使用的AES模式(如CBC)和填充方式(PKCS7)与解密代码完全匹配。
- 分块大小要求:必须设置为16的整数倍,否则中间块解密会失败。
- 内存优化:
AudioSource.stream让audioPlayer边接收数据边播放,不会将整个解密后的音频加载到内存,大幅提升大文件加载速度。 - IV状态维护:CBC模式下,每个后续块的IV必须是前一个密文块的最后16字节,否则解密数据会混乱。
内容的提问来源于stack exchange,提问作者SultanKingGD
相关产品推荐
相关产品推荐

