version:1.0.7
bugfixes: update:修改默认登录方式,增加播放加密视频
This commit is contained in:
44
app/CMakeLists.txt
Normal file
44
app/CMakeLists.txt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# For more information about using CMake with Android Studio, read the
|
||||||
|
# documentation: https://d.android.com/studio/projects/add-native-code.html
|
||||||
|
|
||||||
|
# Sets the minimum version of CMake required to build the native library.
|
||||||
|
|
||||||
|
cmake_minimum_required(VERSION 3.4.1)
|
||||||
|
|
||||||
|
# Creates and names a library, sets it as either STATIC
|
||||||
|
# or SHARED, and provides the relative paths to its source code.
|
||||||
|
# You can define multiple libraries, and CMake builds them for you.
|
||||||
|
# Gradle automatically packages shared libraries with your APK.
|
||||||
|
|
||||||
|
add_library( # Sets the name of the library.
|
||||||
|
hnos
|
||||||
|
|
||||||
|
# Sets the library as a shared library.
|
||||||
|
SHARED
|
||||||
|
|
||||||
|
# Provides a relative path to your source file(s).
|
||||||
|
src/main/jni/hnos.cpp)
|
||||||
|
|
||||||
|
# Searches for a specified prebuilt library and stores the path as a
|
||||||
|
# variable. Because CMake includes system libraries in the search path by
|
||||||
|
# default, you only need to specify the name of the public NDK library
|
||||||
|
# you want to add. CMake verifies that the library exists before
|
||||||
|
# completing its build.
|
||||||
|
|
||||||
|
find_library( # Sets the name of the path variable.
|
||||||
|
log-lib
|
||||||
|
|
||||||
|
# Specifies the name of the NDK library that
|
||||||
|
# you want CMake to locate.
|
||||||
|
log)
|
||||||
|
|
||||||
|
# Specifies libraries CMake should link to your target library. You
|
||||||
|
# can link multiple libraries, such as libraries you define in this
|
||||||
|
# build script, prebuilt third-party libraries, or system libraries.
|
||||||
|
|
||||||
|
target_link_libraries( # Specifies the target library.
|
||||||
|
hnos
|
||||||
|
|
||||||
|
# Links the target library to the log library
|
||||||
|
# included in the NDK.
|
||||||
|
${log-lib})
|
||||||
@@ -18,8 +18,8 @@ android {
|
|||||||
//There are no CERT files because If the mini sdk version is 23+, the AGP will ignore the V1 scheme signature.
|
//There are no CERT files because If the mini sdk version is 23+, the AGP will ignore the V1 scheme signature.
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 7
|
versionCode 8
|
||||||
versionName "1.0.6"
|
versionName "1.0.7"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@@ -30,12 +30,17 @@ android {
|
|||||||
/*, "x86_64", "mips", "mips64"*/
|
/*, "x86_64", "mips", "mips64"*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
checkReleaseBuilds false
|
checkReleaseBuilds false
|
||||||
abortOnError false
|
abortOnError false
|
||||||
}
|
}
|
||||||
|
|
||||||
viewBinding{
|
viewBinding {
|
||||||
enabled = true
|
enabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,9 +49,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
externalNativeBuild {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
cmake {
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
path file('CMakeLists.txt')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//签名
|
//签名
|
||||||
@@ -144,6 +150,11 @@ dependencies {
|
|||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation "androidx.multidex:multidex:2.0.1"
|
implementation "androidx.multidex:multidex:2.0.1"
|
||||||
|
|
||||||
|
implementation 'com.google.android.exoplayer:exoplayer:2.14.1'
|
||||||
|
// implementation 'androidx.media3:media3-exoplayer:1.3.1'
|
||||||
|
// implementation 'androidx.media3:media3-exoplayer-dash:1.3.1'
|
||||||
|
// implementation 'androidx.media3:media3-ui:1.3.1'
|
||||||
|
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
|
|||||||
@@ -67,7 +67,10 @@
|
|||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/DialogCloseOnTouchOutside" />
|
android:theme="@style/DialogCloseOnTouchOutside" />
|
||||||
|
<activity
|
||||||
|
android:name=".activity.player.DecryptionPlayerActivity"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.preview.VideoPreviewActivity"
|
android:name=".activity.preview.VideoPreviewActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.hainaos.vc.base.mvvm.BaseViewModel;
|
|||||||
import com.hainaos.vc.bean.LocalVideoInfo;
|
import com.hainaos.vc.bean.LocalVideoInfo;
|
||||||
import com.hainaos.vc.databinding.ActivityCategoryLocalBinding;
|
import com.hainaos.vc.databinding.ActivityCategoryLocalBinding;
|
||||||
import com.hainaos.vc.utils.FileUtils;
|
import com.hainaos.vc.utils.FileUtils;
|
||||||
|
import com.hainaos.vc.utils.VideoUtils;
|
||||||
import com.trello.rxlifecycle4.android.ActivityEvent;
|
import com.trello.rxlifecycle4.android.ActivityEvent;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -16,6 +17,7 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
@@ -58,10 +60,18 @@ public class LocalCategoryViewModel extends BaseViewModel<ActivityCategoryLocalB
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
|
|
||||||
.map(new io.reactivex.rxjava3.functions.Function<List<String>, ArrayList<LocalVideoInfo>>() {
|
.map(new io.reactivex.rxjava3.functions.Function<List<String>, ArrayList<LocalVideoInfo>>() {
|
||||||
@Override
|
@Override
|
||||||
public ArrayList<LocalVideoInfo> apply(List<String> stringList) throws Throwable {
|
public ArrayList<LocalVideoInfo> apply(List<String> stringList) throws Throwable {
|
||||||
ArrayList<LocalVideoInfo> localVideoInfos = stringList.stream().map(new Function<String, LocalVideoInfo>() {
|
ArrayList<LocalVideoInfo> localVideoInfos = stringList.stream()
|
||||||
|
.filter(new Predicate<String>() {
|
||||||
|
@Override
|
||||||
|
public boolean test(String s) {
|
||||||
|
return VideoUtils.isVideoFormat(s) || s.endsWith(".hnv");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(new Function<String, LocalVideoInfo>() {
|
||||||
@Override
|
@Override
|
||||||
public LocalVideoInfo apply(String s) {
|
public LocalVideoInfo apply(String s) {
|
||||||
File videoFile = new File(file.getAbsolutePath() + File.separator + s);
|
File videoFile = new File(file.getAbsolutePath() + File.separator + s);
|
||||||
@@ -69,13 +79,19 @@ public class LocalCategoryViewModel extends BaseViewModel<ActivityCategoryLocalB
|
|||||||
localVideoInfo.setFile_name(FileUtils.getFileNameWithoutExtension(s));
|
localVideoInfo.setFile_name(FileUtils.getFileNameWithoutExtension(s));
|
||||||
localVideoInfo.setLocalPath(videoFile.getAbsolutePath());
|
localVideoInfo.setLocalPath(videoFile.getAbsolutePath());
|
||||||
|
|
||||||
long time = System.currentTimeMillis();
|
|
||||||
FFmpegMediaMetadataRetriever mmr = new FFmpegMediaMetadataRetriever();
|
FFmpegMediaMetadataRetriever mmr = new FFmpegMediaMetadataRetriever();
|
||||||
|
try {
|
||||||
|
long time = System.currentTimeMillis();
|
||||||
mmr.setDataSource(videoFile.getAbsolutePath());
|
mmr.setDataSource(videoFile.getAbsolutePath());
|
||||||
int duration = Integer.parseInt(mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_DURATION));
|
int duration = Integer.parseInt(mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_DURATION));
|
||||||
Log.e("AudioUtils", "getDurationInMilliseconds: " + (System.currentTimeMillis() - time));
|
Log.e("AudioUtils", "getDurationInMilliseconds: " + (System.currentTimeMillis() - time));
|
||||||
mmr.release();//释放资源
|
|
||||||
localVideoInfo.setDuration(duration / 1000);
|
localVideoInfo.setDuration(duration / 1000);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "apply: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
mmr.release();//释放资源
|
||||||
|
}
|
||||||
|
|
||||||
return localVideoInfo;
|
return localVideoInfo;
|
||||||
}
|
}
|
||||||
}).collect(Collectors.toCollection(ArrayList::new));
|
}).collect(Collectors.toCollection(ArrayList::new));
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public class LoginActivity extends BaseMvvmActivity<LoginViewModel, ActivityLogi
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initView() {
|
protected void initView() {
|
||||||
|
mViewDataBinding.setLoginMode(mLoginMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -134,7 +134,7 @@ public class LoginActivity extends BaseMvvmActivity<LoginViewModel, ActivityLogi
|
|||||||
mViewDataBinding.cardView.setEnabled(true);
|
mViewDataBinding.cardView.setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int mLoginMode = 0;
|
private int mLoginMode = 1;
|
||||||
|
|
||||||
public class BtnClick {
|
public class BtnClick {
|
||||||
public void changeLoginMode(View view) {
|
public void changeLoginMode(View view) {
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package com.hainaos.vc.activity.player;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import com.hainaos.vc.R;
|
||||||
|
import com.hainaos.vc.base.mvvm.BaseMvvmActivity;
|
||||||
|
import com.hainaos.vc.databinding.ActivityDecryptionPlayerBinding;
|
||||||
|
import com.hainaos.vc.utils.JgyUtils;
|
||||||
|
import com.hainaos.vc.video.AesDataSource;
|
||||||
|
import com.hjq.toast.Toaster;
|
||||||
|
|
||||||
|
public class DecryptionPlayerActivity extends BaseMvvmActivity<DecryptionPlayerViewModel, ActivityDecryptionPlayerBinding> {
|
||||||
|
private static final String TAG = "DecryptionPlayer";
|
||||||
|
|
||||||
|
private ExoPlayer mExoPlayer;
|
||||||
|
private String mUrl;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getLayoutId() {
|
||||||
|
return R.layout.activity_decryption_player;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initDataBinding() {
|
||||||
|
mViewModel.setCtx(this);
|
||||||
|
mViewModel.setVDBinding(mViewDataBinding);
|
||||||
|
mViewModel.setLifecycle(getLifecycleSubject());
|
||||||
|
mViewDataBinding.setClick(new BtnClick());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initView() {
|
||||||
|
mExoPlayer = new SimpleExoPlayer.Builder(this).build();
|
||||||
|
mViewDataBinding.playerView.setPlayer(mExoPlayer);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initData() {
|
||||||
|
Intent intent = getIntent();
|
||||||
|
mUrl = intent.getStringExtra("url");
|
||||||
|
if (!TextUtils.isEmpty(mUrl)) {
|
||||||
|
Uri videoUri = Uri.parse(mUrl);
|
||||||
|
MediaItem mediaItem = MediaItem.fromUri(videoUri);
|
||||||
|
|
||||||
|
byte[] key = JgyUtils.getSecretKey().getBytes();
|
||||||
|
byte[] iv = JgyUtils.getIvParameter().getBytes();
|
||||||
|
|
||||||
|
// SecretKeySpec keySpec = new SecretKeySpec(JgyUtils.getSecretKey().getBytes(), "AES");
|
||||||
|
// IvParameterSpec ivSpec = new IvParameterSpec(JgyUtils.getIvParameter().getBytes());
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||||
|
// cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
||||||
|
|
||||||
|
// 创建自定义 Factory
|
||||||
|
DataSource.Factory dataSourceFactory = () -> {
|
||||||
|
try {
|
||||||
|
return new AesDataSource(key, iv);
|
||||||
|
// DataSource upstream = new FileDataSource();
|
||||||
|
// return new AesDataSource2(upstream, key, iv);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
MediaSource mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||||
|
.createMediaSource(mediaItem);
|
||||||
|
mExoPlayer.setMediaSource(mediaSource);
|
||||||
|
mExoPlayer.prepare();
|
||||||
|
mExoPlayer.play();
|
||||||
|
// } catch (Exception e) {
|
||||||
|
// Log.e(TAG, "initData: " + e.getMessage());
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
Toaster.show("文件地址为空");
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
// 在 Android 7.0 (API 24) 及以上,在 onStart 中初始化
|
||||||
|
if (Util.SDK_INT >= 24) {
|
||||||
|
initializePlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
// 在 Android 7.0 以下,在 onResume 中初始化
|
||||||
|
if (Util.SDK_INT < 24) {
|
||||||
|
initializePlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
// 在 Android 7.0 以下,在 onPause 中释放
|
||||||
|
if (Util.SDK_INT < 24) {
|
||||||
|
releasePlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
// 在 Android 7.0 及以上,在 onStop 中释放
|
||||||
|
if (Util.SDK_INT >= 24) {
|
||||||
|
releasePlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializePlayer() {
|
||||||
|
// if (mExoPlayer == null) {
|
||||||
|
// // 1. 创建 ExoPlayer 实例
|
||||||
|
// mExoPlayer = new ExoPlayer.Builder(this).build();
|
||||||
|
// // 2. 将播放器绑定到视图
|
||||||
|
// playerView.setPlayer(mExoPlayer);
|
||||||
|
// // 3. 恢复之前的播放状态(如果有)
|
||||||
|
// mExoPlayer.setPlayWhenReady(playWhenReady);
|
||||||
|
// mExoPlayer.seekTo(currentMediaItemIndex, playbackPosition);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 4. 准备媒体源
|
||||||
|
// Uri videoUri = Uri.parse("https://your-video-url.com/sample.mp4"); // 替换为你的视频URL
|
||||||
|
// MediaItem mediaItem = MediaItem.fromUri(videoUri);
|
||||||
|
// mExoPlayer.setMediaItem(mediaItem);
|
||||||
|
// mExoPlayer.prepare();
|
||||||
|
// // 如果需要自动播放,可以调用 exoPlayer.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releasePlayer() {
|
||||||
|
if (mExoPlayer != null) {
|
||||||
|
// 记录当前的播放状态
|
||||||
|
// playbackPosition = mExoPlayer.getCurrentPosition();
|
||||||
|
// currentMediaItemIndex = mExoPlayer.getCurrentMediaItemIndex();
|
||||||
|
// playWhenReady = mExoPlayer.getPlayWhenReady();
|
||||||
|
|
||||||
|
// 释放播放器资源
|
||||||
|
mExoPlayer.release();
|
||||||
|
mExoPlayer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BtnClick {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.hainaos.vc.activity.player;
|
||||||
|
|
||||||
|
import com.hainaos.vc.base.mvvm.BaseViewModel;
|
||||||
|
import com.hainaos.vc.databinding.ActivityDecryptionPlayerBinding;
|
||||||
|
import com.trello.rxlifecycle4.android.ActivityEvent;
|
||||||
|
|
||||||
|
public class DecryptionPlayerViewModel extends BaseViewModel<ActivityDecryptionPlayerBinding, ActivityEvent> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ActivityDecryptionPlayerBinding getVDBinding() {
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import com.arialyy.annotations.Download;
|
|||||||
import com.arialyy.aria.core.Aria;
|
import com.arialyy.aria.core.Aria;
|
||||||
import com.arialyy.aria.core.task.DownloadTask;
|
import com.arialyy.aria.core.task.DownloadTask;
|
||||||
import com.hainaos.vc.R;
|
import com.hainaos.vc.R;
|
||||||
|
import com.hainaos.vc.activity.player.DecryptionPlayerActivity;
|
||||||
import com.hainaos.vc.activity.preview.VideoPreviewActivity;
|
import com.hainaos.vc.activity.preview.VideoPreviewActivity;
|
||||||
import com.hainaos.vc.bean.CategoryVideoInfo;
|
import com.hainaos.vc.bean.CategoryVideoInfo;
|
||||||
import com.hainaos.vc.config.Permissions;
|
import com.hainaos.vc.config.Permissions;
|
||||||
@@ -103,6 +104,7 @@ public class CategoryVideoAdapter extends RecyclerView.Adapter<CategoryVideoAdap
|
|||||||
} else {
|
} else {
|
||||||
if (XXPermissions.isGranted(mContext, Permissions.STORAGE_PERMISSIONS)) {
|
if (XXPermissions.isGranted(mContext, Permissions.STORAGE_PERMISSIONS)) {
|
||||||
FileUtils.ariaDownload(mContext, mDirName, url, md5);
|
FileUtils.ariaDownload(mContext, mDirName, url, md5);
|
||||||
|
// FileUtils.ariaDownloadCover(mContext, mDirName, cover, md5);
|
||||||
} else {
|
} else {
|
||||||
showPermissionsDialog(mContext);
|
showPermissionsDialog(mContext);
|
||||||
}
|
}
|
||||||
@@ -113,10 +115,16 @@ public class CategoryVideoAdapter extends RecyclerView.Adapter<CategoryVideoAdap
|
|||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
|
if (file.getAbsolutePath().endsWith(".hnv")) {
|
||||||
|
Intent intent = new Intent(mContext, DecryptionPlayerActivity.class);
|
||||||
|
intent.putExtra("url", file.getAbsolutePath());
|
||||||
|
mContext.startActivity(intent);
|
||||||
|
} else {
|
||||||
Intent intent = new Intent(mContext, VideoPreviewActivity.class);
|
Intent intent = new Intent(mContext, VideoPreviewActivity.class);
|
||||||
intent.putExtra("cover", cover);
|
intent.putExtra("cover", cover);
|
||||||
intent.putExtra("url", file.getAbsolutePath());
|
intent.putExtra("url", file.getAbsolutePath());
|
||||||
mContext.startActivity(intent);
|
mContext.startActivity(intent);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Toaster.show("请先下载视频");
|
Toaster.show("请先下载视频");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import com.bumptech.glide.Glide;
|
|||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
import com.hainaos.vc.R;
|
import com.hainaos.vc.R;
|
||||||
|
import com.hainaos.vc.activity.player.DecryptionPlayerActivity;
|
||||||
import com.hainaos.vc.activity.tiktok.TikTokActivity;
|
import com.hainaos.vc.activity.tiktok.TikTokActivity;
|
||||||
import com.hainaos.vc.bean.LocalVideoInfo;
|
import com.hainaos.vc.bean.LocalVideoInfo;
|
||||||
import com.hainaos.vc.utils.FileUtils;
|
import com.hainaos.vc.utils.FileUtils;
|
||||||
@@ -102,8 +103,14 @@ public class VideoAdapter extends RecyclerView.Adapter<VideoAdapter.VideoHolder>
|
|||||||
holder.root.setOnClickListener(new View.OnClickListener() {
|
holder.root.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
|
if (file.getAbsolutePath().endsWith(".hnv")) {
|
||||||
|
Intent intent = new Intent(mContext, DecryptionPlayerActivity.class);
|
||||||
|
intent.putExtra("url", file.getAbsolutePath());
|
||||||
|
mContext.startActivity(intent);
|
||||||
|
} else {
|
||||||
playVideo(position);
|
playVideo(position);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
holder.root.setOnLongClickListener(view -> {
|
holder.root.setOnLongClickListener(view -> {
|
||||||
onItemLongClickListener.onItemLongClick(localPath, position);
|
onItemLongClickListener.onItemLongClick(localPath, position);
|
||||||
|
|||||||
@@ -89,6 +89,41 @@ public class FileUtils {
|
|||||||
return path + File.separator;
|
return path + File.separator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void ariaDownloadCover(Context context, String dirName, String fileName, String url, String md5) {
|
||||||
|
String downLoadPath = getHainaVideoPath(context) + dirName + File.separator;
|
||||||
|
File dirFile = new File(downLoadPath);
|
||||||
|
if (!dirFile.exists()) {
|
||||||
|
Log.e(TAG, "ariaDownload: mkdirs = " + dirFile.mkdirs());
|
||||||
|
}
|
||||||
|
File file = new File(downLoadPath + fileName);
|
||||||
|
if (file.exists() && !file.isDirectory()) {
|
||||||
|
String fileMD5 = com.blankj.utilcode.util.FileUtils.getFileMD5ToString(file);
|
||||||
|
Log.e("ariaDownload", "fileOnlineMD5=" + md5);
|
||||||
|
Log.e("ariaDownload", "fileMD5=" + fileMD5);
|
||||||
|
if (!TextUtils.isEmpty(md5)) {
|
||||||
|
if (!md5.equals(fileMD5)) {
|
||||||
|
Aria.download(context)
|
||||||
|
.load(url) //读取下载地址
|
||||||
|
.setFilePath(file.getAbsolutePath())
|
||||||
|
// .ignoreFilePathOccupy()
|
||||||
|
.setExtendField(url)
|
||||||
|
.create(); //启动下载}
|
||||||
|
} else {
|
||||||
|
Log.e("ariaDownload", "fileName = " + fileName + " exists");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e("ariaDownload", url + " no md5 params , skip");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Aria.download(context)
|
||||||
|
.load(url) //读取下载地址
|
||||||
|
.setFilePath(file.getAbsolutePath())
|
||||||
|
// .ignoreFilePathOccupy()
|
||||||
|
.setExtendField(url)
|
||||||
|
.create(); //启动下载}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void ariaDownload(Context context, String dirName, String url, String md5) {
|
public static void ariaDownload(Context context, String dirName, String url, String md5) {
|
||||||
String downLoadPath = getHainaVideoPath(context) + dirName + File.separator;
|
String downLoadPath = getHainaVideoPath(context) + dirName + File.separator;
|
||||||
File dirFile = new File(downLoadPath);
|
File dirFile = new File(downLoadPath);
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ public class JgyUtils {
|
|||||||
private static JgyUtils sInstance;
|
private static JgyUtils sInstance;
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
|
|
||||||
|
static {
|
||||||
|
System.loadLibrary("hnos");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static native String getSecretKey();
|
||||||
|
|
||||||
|
public static native String getIvParameter();
|
||||||
|
|
||||||
private JgyUtils(Context context) {
|
private JgyUtils(Context context) {
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.hainaos.vc.utils;
|
package com.hainaos.vc.utils;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -27,6 +29,9 @@ public class VideoUtils {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public static boolean isVideoFormat(String filePath) {
|
public static boolean isVideoFormat(String filePath) {
|
||||||
|
if (TextUtils.isEmpty(filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
for (String s : video_extension) {
|
for (String s : video_extension) {
|
||||||
if (filePath.endsWith(s)) {
|
if (filePath.endsWith(s)) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
166
app/src/main/java/com/hainaos/vc/video/AesCtrFileUtil.java
Normal file
166
app/src/main/java/com/hainaos/vc/video/AesCtrFileUtil.java
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package com.hainaos.vc.video;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.CipherOutputStream;
|
||||||
|
import javax.crypto.KeyGenerator;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
public class AesCtrFileUtil {
|
||||||
|
|
||||||
|
private static final String ALGORITHM = "AES";
|
||||||
|
private static final String TRANSFORMATION = "AES/CTR/NoPadding";
|
||||||
|
private static final int IV_SIZE = 16; // AES block size
|
||||||
|
private static final int BUFFER_SIZE = 8192; // 8KB buffer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密文件
|
||||||
|
* 逻辑:生成随机IV -> 写入IV到文件头 -> 读取源文件 -> 加密 -> 写入密文
|
||||||
|
*
|
||||||
|
* @param keySecret AES密钥 (16, 24, or 32 bytes)
|
||||||
|
* @param inputFile 源文件路径
|
||||||
|
* @param outputFile 输出文件路径
|
||||||
|
*/
|
||||||
|
public static void encryptFile(byte[] keySecret, File inputFile, File outputFile)
|
||||||
|
throws GeneralSecurityException, IOException {
|
||||||
|
|
||||||
|
// 1. 准备密钥
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(keySecret, ALGORITHM);
|
||||||
|
|
||||||
|
// 2. 生成随机 IV
|
||||||
|
// byte[] iv = new byte[IV_SIZE];
|
||||||
|
byte[] iv = "hainaos1hainaos1".getBytes();
|
||||||
|
new SecureRandom().nextBytes(iv);
|
||||||
|
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
||||||
|
|
||||||
|
// 3. 初始化 Cipher
|
||||||
|
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
||||||
|
|
||||||
|
// 4. 流式读写
|
||||||
|
try (FileInputStream inputStream = new FileInputStream(inputFile);
|
||||||
|
FileOutputStream outputStream = new FileOutputStream(outputFile)) {
|
||||||
|
|
||||||
|
// IMPORTANT: 先将 IV 写入输出文件的头部,解密时需要读取它
|
||||||
|
outputStream.write(iv);
|
||||||
|
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||||
|
// update 方法处理数据块
|
||||||
|
byte[] output = cipher.update(buffer, 0, bytesRead);
|
||||||
|
if (output != null) {
|
||||||
|
outputStream.write(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// doFinal 处理最后的数据(虽然 NoPadding 通常不需要,但必须调用以完成操作)
|
||||||
|
byte[] outputBytes = cipher.doFinal();
|
||||||
|
if (outputBytes != null) {
|
||||||
|
outputStream.write(outputBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密文件
|
||||||
|
* 逻辑:读取文件头IV -> 初始化Cipher -> 读取密文 -> 解密 -> 写入原文件
|
||||||
|
*/
|
||||||
|
public static void decryptFile(byte[] keySecret, File inputFile, File outputFile)
|
||||||
|
throws GeneralSecurityException, IOException {
|
||||||
|
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(keySecret, ALGORITHM);
|
||||||
|
|
||||||
|
try (FileInputStream inputStream = new FileInputStream(inputFile);
|
||||||
|
FileOutputStream outputStream = new FileOutputStream(outputFile)) {
|
||||||
|
|
||||||
|
// 1. 从文件头部读取 IV
|
||||||
|
byte[] iv = new byte[IV_SIZE];
|
||||||
|
int ivRead = inputStream.read(iv);
|
||||||
|
if (ivRead < IV_SIZE) {
|
||||||
|
throw new IllegalArgumentException("文件太短,无法读取 IV/文件已损坏");
|
||||||
|
}
|
||||||
|
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
||||||
|
|
||||||
|
// 2. 初始化 Cipher (解密模式)
|
||||||
|
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
||||||
|
|
||||||
|
// 3. 流式处理剩余内容
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||||
|
byte[] output = cipher.update(buffer, 0, bytesRead);
|
||||||
|
if (output != null) {
|
||||||
|
outputStream.write(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byte[] outputBytes = cipher.doFinal();
|
||||||
|
if (outputBytes != null) {
|
||||||
|
outputStream.write(outputBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void encryptFile(File inputFile, File outputFile, byte[] key, byte[] iv) throws Exception {
|
||||||
|
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM);
|
||||||
|
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
||||||
|
|
||||||
|
try (FileInputStream fis = new FileInputStream(inputFile);
|
||||||
|
FileOutputStream fos = new FileOutputStream(outputFile);
|
||||||
|
CipherOutputStream cos = new CipherOutputStream(fos, cipher)) {
|
||||||
|
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int read;
|
||||||
|
while ((read = fis.read(buffer)) != -1) {
|
||||||
|
cos.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 测试 Main 方法 ---
|
||||||
|
public static void main(String[] args) {
|
||||||
|
try {
|
||||||
|
// 1. 生成一个测试密钥 (256位)
|
||||||
|
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
|
||||||
|
keyGen.init(256);
|
||||||
|
SecretKey secretKey = keyGen.generateKey();
|
||||||
|
// byte[] keyBytes = secretKey.getEncoded();
|
||||||
|
byte[] keyBytes = "hainaos_key_123_".getBytes();
|
||||||
|
|
||||||
|
// 2. 准备文件
|
||||||
|
File sourceFile = new File("C:\\Users\\TT\\Videos\\美妆\\test_video_1.mp4");
|
||||||
|
// File sourceFile = new File("test_original.txt");
|
||||||
|
File encryptedFile = new File("C:\\Users\\TT\\Videos\\美妆\\test_video_1.hnv");
|
||||||
|
File decryptedFile = new File("C:\\Users\\TT\\Videos\\美妆\\test_decrypted.mp4");
|
||||||
|
|
||||||
|
encryptFile(sourceFile, encryptedFile, keyBytes, "hainaos1hainaos1".getBytes());
|
||||||
|
|
||||||
|
// 创建一个简单的测试文件
|
||||||
|
// try (FileWriter writer = new FileWriter(sourceFile)) {
|
||||||
|
// writer.write("Hello, World! This is a test for AES/CTR/NoPadding
|
||||||
|
// encryption.");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// System.out.println("开始加密...");
|
||||||
|
// encryptFile(keyBytes, sourceFile, encryptedFile);
|
||||||
|
// System.out.println("加密完成: " + encryptedFile.getAbsolutePath());
|
||||||
|
|
||||||
|
// System.out.println("开始解密...");
|
||||||
|
// decryptFile(keyBytes, encryptedFile, decryptedFile);
|
||||||
|
// System.out.println("解密完成: " + decryptedFile.getAbsolutePath());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/src/main/java/com/hainaos/vc/video/AesDataSource.java
Normal file
69
app/src/main/java/com/hainaos/vc/video/AesDataSource.java
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package com.hainaos.vc.video;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.CipherInputStream;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
public class AesDataSource implements DataSource {
|
||||||
|
private final Cipher cipher;
|
||||||
|
private InputStream inputStream;
|
||||||
|
private Uri uri;
|
||||||
|
|
||||||
|
public AesDataSource(byte[] key, byte[] iv) throws Exception {
|
||||||
|
cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
|
||||||
|
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long open(DataSpec dataSpec) throws IOException {
|
||||||
|
this.uri = dataSpec.uri;
|
||||||
|
// 注意:如果是网络流,这里需要使用 HttpURLConnection
|
||||||
|
// 示例代码以本地文件为例
|
||||||
|
FileInputStream fis = new FileInputStream(uri.getPath());
|
||||||
|
inputStream = new CipherInputStream(fis, cipher);
|
||||||
|
|
||||||
|
// 如果支持 Seek(拖动进度条),此处需处理 skip
|
||||||
|
if (dataSpec.position > 0) {
|
||||||
|
inputStream.skip(dataSpec.position);
|
||||||
|
}
|
||||||
|
return dataSpec.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer, int offset, int readLength) throws IOException {
|
||||||
|
if (readLength == 0) return 0;
|
||||||
|
int read = inputStream.read(buffer, offset, readLength);
|
||||||
|
if (read == -1) return -1;
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getUri() {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
if (inputStream != null) {
|
||||||
|
inputStream.close();
|
||||||
|
inputStream = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTransferListener(TransferListener transferListener) {
|
||||||
|
}
|
||||||
|
}
|
||||||
200
app/src/main/java/com/hainaos/vc/video/AesDataSource2.java
Normal file
200
app/src/main/java/com/hainaos/vc/video/AesDataSource2.java
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package com.hainaos.vc.video;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.CipherInputStream;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
public class AesDataSource2 implements DataSource {
|
||||||
|
|
||||||
|
private final DataSource upstream;
|
||||||
|
private final byte[] secretKey;
|
||||||
|
private final byte[] iv;
|
||||||
|
|
||||||
|
private CipherInputStream cipherInputStream;
|
||||||
|
private long bytesRemaining;
|
||||||
|
private boolean opened;
|
||||||
|
|
||||||
|
public AesDataSource2(DataSource upstream, byte[] secretKey, byte[] iv) {
|
||||||
|
this.upstream = upstream;
|
||||||
|
this.secretKey = secretKey;
|
||||||
|
this.iv = iv;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTransferListener(TransferListener transferListener) {
|
||||||
|
upstream.addTransferListener(transferListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long open(DataSpec dataSpec) throws IOException {
|
||||||
|
// 1. 获取请求的绝对位置
|
||||||
|
long position = dataSpec.position;
|
||||||
|
|
||||||
|
// AES 块大小通常为 16 字节
|
||||||
|
final int AES_BLOCK_SIZE = 16;
|
||||||
|
|
||||||
|
// 2. 计算块索引 (Block Index) 和 块内偏移 (Offset inside the block)
|
||||||
|
// 例如:position = 100,blockIndex = 6 (96字节处),offset = 4
|
||||||
|
long blockIndex = position / AES_BLOCK_SIZE;
|
||||||
|
int offsetInBlock = (int) (position % AES_BLOCK_SIZE);
|
||||||
|
|
||||||
|
// 3. 计算对齐后的起始读取位置 (必须是 16 的倍数)
|
||||||
|
long startPosition = blockIndex * AES_BLOCK_SIZE;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 4. 初始化 Cipher
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(secretKey, "AES");
|
||||||
|
|
||||||
|
// 【关键步骤】:根据 blockIndex 计算新的 IV
|
||||||
|
// CTR 模式下:NewIV = OriginalIV + blockIndex
|
||||||
|
byte[] newIv = getAdjustedIv(this.iv, blockIndex);
|
||||||
|
IvParameterSpec ivSpec = new IvParameterSpec(newIv);
|
||||||
|
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
||||||
|
|
||||||
|
// 5. 让上层数据源 (FileDataSource) 从对齐的位置 (startPosition) 开始读
|
||||||
|
// 注意:我们修改了 position,但保持 length 不变 (或者处理 open ended)
|
||||||
|
long length = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length + offsetInBlock : C.LENGTH_UNSET;
|
||||||
|
|
||||||
|
DataSpec newSpec = dataSpec.buildUpon()
|
||||||
|
.setPosition(startPosition)
|
||||||
|
.setLength(length)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 打开上层流
|
||||||
|
InputStream inputStream = new DataSourceInputStream(upstream, newSpec);
|
||||||
|
|
||||||
|
// 创建解密流
|
||||||
|
cipherInputStream = new CipherInputStream(inputStream, cipher);
|
||||||
|
|
||||||
|
// 6. 【重要】跳过块内的偏移量
|
||||||
|
// 因为我们要给 ExoPlayer 返回的是从 dataSpec.position 开始的数据,
|
||||||
|
// 但我们是从 startPosition (前一个16倍数) 开始解密的,所以前面多读的 offsetInBlock 个字节是无用的。
|
||||||
|
if (offsetInBlock > 0) {
|
||||||
|
forceSkip(cipherInputStream, offsetInBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
opened = true;
|
||||||
|
|
||||||
|
// 计算剩余长度
|
||||||
|
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||||
|
bytesRemaining = dataSpec.length;
|
||||||
|
} else {
|
||||||
|
long upstreamLength = upstream.open(newSpec); // 这一步其实已经在 DataSourceInputStream 里做过了,这里仅作逻辑参考
|
||||||
|
// 通常 upstream.open 返回的是从 startPosition 开始的长度
|
||||||
|
// 如果 upstream 支持长度解析:
|
||||||
|
// bytesRemaining = upstreamLength != C.LENGTH_UNSET ? upstreamLength - offsetInBlock : C.LENGTH_UNSET;
|
||||||
|
bytesRemaining = C.LENGTH_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytesRemaining;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据块索引计算新的 IV。
|
||||||
|
* CTR 模式将 IV 视为一个大整数 (BigEndian),每过一个块,计数器 +1。
|
||||||
|
*/
|
||||||
|
private byte[] getAdjustedIv(byte[] originalIv, long blockIndex) {
|
||||||
|
// 使用 BigInteger 处理大数加法,防止溢出
|
||||||
|
BigInteger ivVal = new BigInteger(1, originalIv);
|
||||||
|
BigInteger offset = BigInteger.valueOf(blockIndex);
|
||||||
|
BigInteger newIvVal = ivVal.add(offset);
|
||||||
|
|
||||||
|
byte[] newIv = newIvVal.toByteArray();
|
||||||
|
|
||||||
|
// BigInteger.toByteArray() 可能会因为符号位导致长度变为 17 (如果是正数且最高位是1)
|
||||||
|
// 或者因为数值较小导致长度小于 16。必须确保返回 16 字节。
|
||||||
|
byte[] result = new byte[16];
|
||||||
|
int srcOffset = newIv.length > 16 ? newIv.length - 16 : 0;
|
||||||
|
int dstOffset = newIv.length < 16 ? 16 - newIv.length : 0;
|
||||||
|
int copyLength = newIv.length > 16 ? 16 : newIv.length;
|
||||||
|
|
||||||
|
System.arraycopy(newIv, srcOffset, result, dstOffset, copyLength);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制跳过指定字节数。CipherInputStream 的 skip 有时不可靠,建议循环 read。
|
||||||
|
*/
|
||||||
|
private void forceSkip(InputStream stream, int bytesToSkip) throws IOException {
|
||||||
|
long skipped = 0;
|
||||||
|
byte[] skipBuffer = new byte[1024];
|
||||||
|
while (skipped < bytesToSkip) {
|
||||||
|
int toRead = (int) Math.min(bytesToSkip - skipped, skipBuffer.length);
|
||||||
|
int read = stream.read(skipBuffer, 0, toRead);
|
||||||
|
if (read == -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
skipped += read;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer, int offset, int readLength) throws IOException {
|
||||||
|
if (readLength == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int bytesToRead = readLength;
|
||||||
|
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||||
|
bytesToRead = (int) Math.min(readLength, bytesRemaining);
|
||||||
|
}
|
||||||
|
if (bytesToRead == 0) {
|
||||||
|
return C.RESULT_END_OF_INPUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bytesRead = cipherInputStream.read(buffer, offset, bytesToRead);
|
||||||
|
if (bytesRead == -1) {
|
||||||
|
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||||
|
// 预期还要读数据但读不到了,抛错
|
||||||
|
throw new IOException("End of stream reached prematurely");
|
||||||
|
}
|
||||||
|
return C.RESULT_END_OF_INPUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||||
|
bytesRemaining -= bytesRead;
|
||||||
|
}
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getUri() {
|
||||||
|
return upstream.getUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, List<String>> getResponseHeaders() {
|
||||||
|
return upstream.getResponseHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
if (opened) {
|
||||||
|
opened = false;
|
||||||
|
if (cipherInputStream != null) {
|
||||||
|
cipherInputStream.close(); // 这也会关闭 upstream
|
||||||
|
cipherInputStream = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.hainaos.vc.video;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
|
||||||
|
public class DecryptingDataSource implements DataSource {
|
||||||
|
private final DataSource upstream;
|
||||||
|
private final Cipher cipher;
|
||||||
|
|
||||||
|
public DecryptingDataSource(DataSource upstream, Cipher cipher) {
|
||||||
|
this.upstream = upstream;
|
||||||
|
this.cipher = cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTransferListener(TransferListener transferListener) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long open(DataSpec dataSpec) throws IOException {
|
||||||
|
// 初始化解密状态,比如根据 dataSpec.position 调整 Cipher 的偏移量
|
||||||
|
return upstream.open(dataSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer, int offset, int readLength) throws IOException {
|
||||||
|
if (readLength == 0) return 0;
|
||||||
|
|
||||||
|
// 1. 从原始源(文件或网络)读取加密数据
|
||||||
|
int bytesRead = upstream.read(buffer, offset, readLength);
|
||||||
|
if (bytesRead == -1) return -1;
|
||||||
|
|
||||||
|
// 2. 原地解密 (In-place decryption)
|
||||||
|
// 注意:此处需要处理流式解密的块对齐问题
|
||||||
|
byte[] decryptedData = cipher.update(buffer, offset, bytesRead);
|
||||||
|
if (decryptedData != null) {
|
||||||
|
System.arraycopy(decryptedData, 0, buffer, offset, decryptedData.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getUri() {
|
||||||
|
return upstream.getUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
upstream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.hainaos.vc.video;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
|
||||||
|
public class DecryptingDataSourceFactory implements DataSource.Factory {
|
||||||
|
private final Context context;
|
||||||
|
private final Cipher cipher;
|
||||||
|
|
||||||
|
public DecryptingDataSourceFactory(Context context, Cipher cipher) {
|
||||||
|
this.context = context;
|
||||||
|
this.cipher = cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataSource createDataSource() {
|
||||||
|
// 通常包装一个 DefaultDataSource 用于支持本地文件和网络
|
||||||
|
DataSource upstream = new DefaultDataSource(context, "User-Agent", false);
|
||||||
|
return new DecryptingDataSource(upstream, cipher);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/src/main/java/com/hainaos/vc/video/DecryptionPlay.java
Normal file
40
app/src/main/java/com/hainaos/vc/video/DecryptionPlay.java
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package com.hainaos.vc.video;
|
||||||
|
|
||||||
|
public class DecryptionPlay {
|
||||||
|
private static final String ALGORITHM = "AES/CTR/NoPadding";
|
||||||
|
|
||||||
|
// public void testCode(Context context) {
|
||||||
|
// SecretKeySpec keySpec = new SecretKeySpec(JgyUtils.getSecretKey().getBytes(), "AES");
|
||||||
|
// IvParameterSpec ivSpec = new IvParameterSpec(JgyUtils.getIvParameter().getBytes());
|
||||||
|
//
|
||||||
|
// // 初始化 Cipher (例如 AES/CTR/NoPadding)
|
||||||
|
// Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||||
|
// cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
||||||
|
// DataSource.Factory dataSourceFactory = new DecryptingDataSourceFactory(context, cipher);
|
||||||
|
// MediaSource mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||||
|
// .createMediaSource(MediaItem.fromUri(encryptedUri));
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// // 1. 准备密钥和 IV (需与加密时一致)
|
||||||
|
// byte[] secretKey = "your_16_byte_key".getBytes();
|
||||||
|
// byte[] iv = "your_16_byte_iv__".getBytes();
|
||||||
|
//
|
||||||
|
// // 2. 创建用于解密的 DataSource 工厂
|
||||||
|
// DataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context) ;
|
||||||
|
// DataSource.Factory decryptingDataSourceFactory = () ->
|
||||||
|
// new AesCipherDataSource(secretKey, upstreamFactory.createDataSource());
|
||||||
|
//
|
||||||
|
// // 3. 构建播放列表项并设置 MediaSource
|
||||||
|
// ExoPlayer player = new ExoPlayer.Builder(context).build();
|
||||||
|
// MediaItem mediaItem = MediaItem.fromUri(Uri.fromFile(encryptedFile));
|
||||||
|
//
|
||||||
|
// MediaSource mediaSource = new ProgressiveMediaSource.Factory(decryptingDataSourceFactory)
|
||||||
|
// .createMediaSource(mediaItem);
|
||||||
|
//
|
||||||
|
// player.setMediaSource(mediaSource);
|
||||||
|
// player.prepare();
|
||||||
|
// player.play();
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
36
app/src/main/java/com/hainaos/vc/video/VideoEncryptor.java
Normal file
36
app/src/main/java/com/hainaos/vc/video/VideoEncryptor.java
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package com.hainaos.vc.video;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.CipherOutputStream;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
public class VideoEncryptor {
|
||||||
|
private static final String ALGORITHM = "AES/CTR/NoPadding";
|
||||||
|
|
||||||
|
// 注意:实际开发中,Key 和 IV 应该安全存储,不要硬编码
|
||||||
|
public static void encryptVideo(File inputFile, File outputFile, byte[] key, byte[] iv) throws Exception {
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
|
||||||
|
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
||||||
|
|
||||||
|
try (FileInputStream fis = new FileInputStream(inputFile);
|
||||||
|
FileOutputStream fos = new FileOutputStream(outputFile);
|
||||||
|
CipherOutputStream cos = new CipherOutputStream(fos, cipher)) {
|
||||||
|
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = fis.read(buffer)) != -1) {
|
||||||
|
cos.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
22
app/src/main/jni/hnos.cpp
Normal file
22
app/src/main/jni/hnos.cpp
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#include <jni.h>
|
||||||
|
#include <string>
|
||||||
|
#include <sys/system_properties.h>
|
||||||
|
// 日志打印
|
||||||
|
#include <android/log.h>
|
||||||
|
|
||||||
|
#define LOG_TAG "TAG_LOG"
|
||||||
|
#define LOGI(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT jstring JNICALL
|
||||||
|
Java_com_hainaos_vc_utils_JgyUtils_getSecretKey(JNIEnv *env, jclass clazz) {
|
||||||
|
std::string key = "hainaos_key_123_";
|
||||||
|
return env->NewStringUTF(key.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT jstring JNICALL
|
||||||
|
Java_com_hainaos_vc_utils_JgyUtils_getIvParameter(JNIEnv *env, jclass clazz) {
|
||||||
|
std::string key = "hainaos1hainaos1";
|
||||||
|
return env->NewStringUTF(key.c_str());
|
||||||
|
}
|
||||||
23
app/src/main/res/layout/activity_decryption_player.xml
Normal file
23
app/src/main/res/layout/activity_decryption_player.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:context=".activity.player.DecryptionPlayerActivity">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="click"
|
||||||
|
type="com.hainaos.vc.activity.player.DecryptionPlayerActivity.BtnClick" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.exoplayer2.ui.PlayerView
|
||||||
|
android:id="@+id/player_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</layout>
|
||||||
@@ -27,11 +27,11 @@ class VideoEncryptorGUI:
|
|||||||
# Key输入
|
# Key输入
|
||||||
self.label_key = tk.Label(master, text="AES密钥 (Base64编码或16/24/32字节字符串):")
|
self.label_key = tk.Label(master, text="AES密钥 (Base64编码或16/24/32字节字符串):")
|
||||||
self.label_key.pack(pady=(10,0))
|
self.label_key.pack(pady=(10,0))
|
||||||
self.entry_key = tk.Entry(master, width=50, show="*")
|
self.entry_key = tk.Entry(master, width=50)
|
||||||
self.entry_key.pack(pady=5)
|
self.entry_key.pack(pady=5)
|
||||||
|
|
||||||
# IV输入
|
# IV输入
|
||||||
self.label_iv = tk.Label(master, text="初始化向量IV (Base64编码或16字节字符串):")
|
self.label_iv = tk.Label(master, text="初始化向量IV (Base64编码或8字节字符串):")
|
||||||
self.label_iv.pack(pady=(10,0))
|
self.label_iv.pack(pady=(10,0))
|
||||||
self.entry_iv = tk.Entry(master, width=50)
|
self.entry_iv = tk.Entry(master, width=50)
|
||||||
self.entry_iv.pack(pady=5)
|
self.entry_iv.pack(pady=5)
|
||||||
@@ -64,21 +64,21 @@ class VideoEncryptorGUI:
|
|||||||
self.status_label.config(text="文件已选择,请设置密钥")
|
self.status_label.config(text="文件已选择,请设置密钥")
|
||||||
|
|
||||||
def generate_key_iv(self):
|
def generate_key_iv(self):
|
||||||
"""生成随机的密钥和IV"""
|
"""生成随机的密钥和nonce"""
|
||||||
# 生成随机密钥(32字节,AES-256)和IV(16字节)
|
# 生成随机密钥(32字节,AES-256)和nonce(8字节用于CTR模式)
|
||||||
key = get_random_bytes(32)
|
key = get_random_bytes(32)
|
||||||
iv = get_random_bytes(16)
|
nonce = get_random_bytes(8) # AES CTR模式使用的nonce应该是8字节
|
||||||
|
|
||||||
# 转换为Base64编码字符串,显示在输入框中
|
# 转换为Base64编码字符串,显示在输入框中
|
||||||
key_b64 = base64.b64encode(key).decode('utf-8')
|
key_b64 = base64.b64encode(key).decode('utf-8')
|
||||||
iv_b64 = base64.b64encode(iv).decode('utf-8')
|
nonce_b64 = base64.b64encode(nonce).decode('utf-8')
|
||||||
|
|
||||||
self.entry_key.delete(0, tk.END)
|
self.entry_key.delete(0, tk.END)
|
||||||
self.entry_key.insert(0, key_b64)
|
self.entry_key.insert(0, key_b64)
|
||||||
self.entry_iv.delete(0, tk.END)
|
self.entry_iv.delete(0, tk.END)
|
||||||
self.entry_iv.insert(0, iv_b64)
|
self.entry_iv.insert(0, nonce_b64)
|
||||||
|
|
||||||
self.status_label.config(text="已生成随机密钥和IV,请妥善保存!")
|
self.status_label.config(text="已生成随机密钥和nonce,请妥善保存!")
|
||||||
|
|
||||||
def encrypt_video(self):
|
def encrypt_video(self):
|
||||||
"""加密视频文件"""
|
"""加密视频文件"""
|
||||||
@@ -94,35 +94,38 @@ class VideoEncryptorGUI:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
# 处理密钥:尝试Base64解码,否则使用字符串编码
|
# 处理密钥:尝试Base64解码,否则使用字符串编码
|
||||||
try:
|
|
||||||
key = base64.b64decode(key_str)
|
# try:
|
||||||
except:
|
# key = base64.b64decode(key_str)
|
||||||
|
# except:
|
||||||
key = key_str.encode('utf-8')
|
key = key_str.encode('utf-8')
|
||||||
|
|
||||||
# 处理IV:尝试Base64解码,否则使用字符串编码
|
# 处理IV:尝试Base64解码,否则使用字符串编码
|
||||||
try:
|
|
||||||
iv = base64.b64decode(iv_str)
|
# try:
|
||||||
except:
|
# iv = base64.b64decode(iv_str)
|
||||||
|
# except:
|
||||||
iv = iv_str.encode('utf-8')
|
iv = iv_str.encode('utf-8')
|
||||||
|
|
||||||
# 确保密钥长度为16、24或32字节
|
# 确保密钥长度为16、24或32字节
|
||||||
if len(key) not in [16, 24, 32]:
|
# if len(key) not in [16, 24, 32]:
|
||||||
if len(key) < 16:
|
# if len(key) < 16:
|
||||||
key = key.ljust(16, b'\0')
|
# key = key.ljust(16, b'\0')
|
||||||
elif len(key) < 24:
|
# elif len(key) < 24:
|
||||||
key = key.ljust(24, b'\0')
|
# key = key.ljust(24, b'\0')
|
||||||
elif len(key) < 32:
|
# elif len(key) < 32:
|
||||||
key = key.ljust(32, b'\0')
|
# key = key.ljust(32, b'\0')
|
||||||
else:
|
# else:
|
||||||
key = key[:32]
|
# key = key[:32]
|
||||||
|
|
||||||
# 确保IV长度为16字节
|
# 确保IV长度为16字节
|
||||||
if len(iv) != 16:
|
# if len(iv) != 16:
|
||||||
if len(iv) < 16:
|
# if len(iv) < 16:
|
||||||
iv = iv.ljust(16, b'\0')
|
# iv = iv.ljust(16, b'\0')
|
||||||
else:
|
# else:
|
||||||
iv = iv[:16]
|
# iv = iv[:16]
|
||||||
|
|
||||||
# 生成输出文件路径
|
# 生成输出文件路径
|
||||||
file_dir = os.path.dirname(self.input_file_path)
|
file_dir = os.path.dirname(self.input_file_path)
|
||||||
@@ -140,24 +143,30 @@ class VideoEncryptorGUI:
|
|||||||
messagebox.showerror("加密错误", f"加密过程中发生错误: {str(e)}")
|
messagebox.showerror("加密错误", f"加密过程中发生错误: {str(e)}")
|
||||||
|
|
||||||
def do_encryption(self, input_path, output_path, key, iv):
|
def do_encryption(self, input_path, output_path, key, iv):
|
||||||
|
print(f"iv len: {len(iv)}")
|
||||||
|
|
||||||
"""
|
"""
|
||||||
执行加密操作
|
执行加密操作
|
||||||
使用AES/CBC/PKCS5Padding模式,与Java代码保持一致
|
使用AES/CTR/NoPadding模式
|
||||||
"""
|
"""
|
||||||
# 读取输入文件
|
try:
|
||||||
with open(input_path, 'rb') as f_in:
|
# 创建CTR模式加密器
|
||||||
plaintext = f_in.read()
|
cipher = AES.new(key, AES.MODE_CTR, nonce=iv)
|
||||||
|
with open(input_path, 'rb') as fin:
|
||||||
|
with open(output_path, 'wb') as fout:
|
||||||
|
# 将IV写入文件开头,解密时需要
|
||||||
|
fout.write(iv)
|
||||||
|
|
||||||
# 创建密码器
|
# 逐块加密文件
|
||||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
while True:
|
||||||
|
chunk = fin.read(4096) # 每次读取4KB
|
||||||
|
if len(chunk) == 0:
|
||||||
|
break
|
||||||
|
encrypted_chunk = cipher.encrypt(chunk)
|
||||||
|
fout.write(encrypted_chunk)
|
||||||
|
|
||||||
# 应用PKCS5填充并加密
|
except Exception as e:
|
||||||
padded_data = pad(plaintext, AES.block_size)
|
raise Exception(f"加密失败: {str(e)}")
|
||||||
ciphertext = cipher.encrypt(padded_data)
|
|
||||||
|
|
||||||
# 写入输出文件
|
|
||||||
with open(output_path, 'wb') as f_out:
|
|
||||||
f_out.write(ciphertext)
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# 检查所需库是否已安装
|
# 检查所需库是否已安装
|
||||||
38
ui/py/encryption_video.spec
Normal file
38
ui/py/encryption_video.spec
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['encryption_video.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='encryption_video',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user