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.
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 29
|
||||
versionCode 7
|
||||
versionName "1.0.6"
|
||||
versionCode 8
|
||||
versionName "1.0.7"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -30,12 +30,17 @@ android {
|
||||
/*, "x86_64", "mips", "mips64"*/
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
viewBinding{
|
||||
viewBinding {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
@@ -44,9 +49,10 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path file('CMakeLists.txt')
|
||||
}
|
||||
}
|
||||
|
||||
//签名
|
||||
@@ -144,6 +150,11 @@ dependencies {
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
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'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
|
||||
@@ -67,7 +67,10 @@
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/DialogCloseOnTouchOutside" />
|
||||
|
||||
<activity
|
||||
android:name=".activity.player.DecryptionPlayerActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".activity.preview.VideoPreviewActivity"
|
||||
android:launchMode="singleTask"
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.hainaos.vc.base.mvvm.BaseViewModel;
|
||||
import com.hainaos.vc.bean.LocalVideoInfo;
|
||||
import com.hainaos.vc.databinding.ActivityCategoryLocalBinding;
|
||||
import com.hainaos.vc.utils.FileUtils;
|
||||
import com.hainaos.vc.utils.VideoUtils;
|
||||
import com.trello.rxlifecycle4.android.ActivityEvent;
|
||||
|
||||
import java.io.File;
|
||||
@@ -16,6 +17,7 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
@@ -58,27 +60,41 @@ public class LocalCategoryViewModel extends BaseViewModel<ActivityCategoryLocalB
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
||||
.map(new io.reactivex.rxjava3.functions.Function<List<String>, ArrayList<LocalVideoInfo>>() {
|
||||
@Override
|
||||
public ArrayList<LocalVideoInfo> apply(List<String> stringList) throws Throwable {
|
||||
ArrayList<LocalVideoInfo> localVideoInfos = stringList.stream().map(new Function<String, LocalVideoInfo>() {
|
||||
@Override
|
||||
public LocalVideoInfo apply(String s) {
|
||||
File videoFile = new File(file.getAbsolutePath() + File.separator + s);
|
||||
LocalVideoInfo localVideoInfo = new LocalVideoInfo();
|
||||
localVideoInfo.setFile_name(FileUtils.getFileNameWithoutExtension(s));
|
||||
localVideoInfo.setLocalPath(videoFile.getAbsolutePath());
|
||||
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
|
||||
public LocalVideoInfo apply(String s) {
|
||||
File videoFile = new File(file.getAbsolutePath() + File.separator + s);
|
||||
LocalVideoInfo localVideoInfo = new LocalVideoInfo();
|
||||
localVideoInfo.setFile_name(FileUtils.getFileNameWithoutExtension(s));
|
||||
localVideoInfo.setLocalPath(videoFile.getAbsolutePath());
|
||||
|
||||
long time = System.currentTimeMillis();
|
||||
FFmpegMediaMetadataRetriever mmr = new FFmpegMediaMetadataRetriever();
|
||||
mmr.setDataSource(videoFile.getAbsolutePath());
|
||||
int duration = Integer.parseInt(mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_DURATION));
|
||||
Log.e("AudioUtils", "getDurationInMilliseconds: " + (System.currentTimeMillis() - time));
|
||||
mmr.release();//释放资源
|
||||
localVideoInfo.setDuration(duration / 1000);
|
||||
return localVideoInfo;
|
||||
}
|
||||
}).collect(Collectors.toCollection(ArrayList::new));
|
||||
FFmpegMediaMetadataRetriever mmr = new FFmpegMediaMetadataRetriever();
|
||||
try {
|
||||
long time = System.currentTimeMillis();
|
||||
mmr.setDataSource(videoFile.getAbsolutePath());
|
||||
int duration = Integer.parseInt(mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_DURATION));
|
||||
Log.e("AudioUtils", "getDurationInMilliseconds: " + (System.currentTimeMillis() - time));
|
||||
localVideoInfo.setDuration(duration / 1000);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "apply: " + e.getMessage());
|
||||
} finally {
|
||||
mmr.release();//释放资源
|
||||
}
|
||||
|
||||
return localVideoInfo;
|
||||
}
|
||||
}).collect(Collectors.toCollection(ArrayList::new));
|
||||
return localVideoInfos;
|
||||
}
|
||||
})
|
||||
|
||||
@@ -47,7 +47,7 @@ public class LoginActivity extends BaseMvvmActivity<LoginViewModel, ActivityLogi
|
||||
|
||||
@Override
|
||||
protected void initView() {
|
||||
|
||||
mViewDataBinding.setLoginMode(mLoginMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -134,7 +134,7 @@ public class LoginActivity extends BaseMvvmActivity<LoginViewModel, ActivityLogi
|
||||
mViewDataBinding.cardView.setEnabled(true);
|
||||
}
|
||||
|
||||
private int mLoginMode = 0;
|
||||
private int mLoginMode = 1;
|
||||
|
||||
public class BtnClick {
|
||||
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.task.DownloadTask;
|
||||
import com.hainaos.vc.R;
|
||||
import com.hainaos.vc.activity.player.DecryptionPlayerActivity;
|
||||
import com.hainaos.vc.activity.preview.VideoPreviewActivity;
|
||||
import com.hainaos.vc.bean.CategoryVideoInfo;
|
||||
import com.hainaos.vc.config.Permissions;
|
||||
@@ -103,6 +104,7 @@ public class CategoryVideoAdapter extends RecyclerView.Adapter<CategoryVideoAdap
|
||||
} else {
|
||||
if (XXPermissions.isGranted(mContext, Permissions.STORAGE_PERMISSIONS)) {
|
||||
FileUtils.ariaDownload(mContext, mDirName, url, md5);
|
||||
// FileUtils.ariaDownloadCover(mContext, mDirName, cover, md5);
|
||||
} else {
|
||||
showPermissionsDialog(mContext);
|
||||
}
|
||||
@@ -113,10 +115,16 @@ public class CategoryVideoAdapter extends RecyclerView.Adapter<CategoryVideoAdap
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (file.exists()) {
|
||||
Intent intent = new Intent(mContext, VideoPreviewActivity.class);
|
||||
intent.putExtra("cover", cover);
|
||||
intent.putExtra("url", file.getAbsolutePath());
|
||||
mContext.startActivity(intent);
|
||||
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.putExtra("cover", cover);
|
||||
intent.putExtra("url", file.getAbsolutePath());
|
||||
mContext.startActivity(intent);
|
||||
}
|
||||
} else {
|
||||
Toaster.show("请先下载视频");
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.bumptech.glide.Glide;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.hainaos.vc.R;
|
||||
import com.hainaos.vc.activity.player.DecryptionPlayerActivity;
|
||||
import com.hainaos.vc.activity.tiktok.TikTokActivity;
|
||||
import com.hainaos.vc.bean.LocalVideoInfo;
|
||||
import com.hainaos.vc.utils.FileUtils;
|
||||
@@ -102,7 +103,13 @@ public class VideoAdapter extends RecyclerView.Adapter<VideoAdapter.VideoHolder>
|
||||
holder.root.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
playVideo(position);
|
||||
if (file.getAbsolutePath().endsWith(".hnv")) {
|
||||
Intent intent = new Intent(mContext, DecryptionPlayerActivity.class);
|
||||
intent.putExtra("url", file.getAbsolutePath());
|
||||
mContext.startActivity(intent);
|
||||
} else {
|
||||
playVideo(position);
|
||||
}
|
||||
}
|
||||
});
|
||||
holder.root.setOnLongClickListener(view -> {
|
||||
|
||||
@@ -89,6 +89,41 @@ public class FileUtils {
|
||||
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) {
|
||||
String downLoadPath = getHainaVideoPath(context) + dirName + File.separator;
|
||||
File dirFile = new File(downLoadPath);
|
||||
|
||||
@@ -31,6 +31,13 @@ public class JgyUtils {
|
||||
private static JgyUtils sInstance;
|
||||
private Context mContext;
|
||||
|
||||
static {
|
||||
System.loadLibrary("hnos");
|
||||
}
|
||||
|
||||
public static native String getSecretKey();
|
||||
|
||||
public static native String getIvParameter();
|
||||
|
||||
private JgyUtils(Context context) {
|
||||
if (context == null) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.hainaos.vc.utils;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.math.BigInteger;
|
||||
@@ -27,6 +29,9 @@ public class VideoUtils {
|
||||
};
|
||||
|
||||
public static boolean isVideoFormat(String filePath) {
|
||||
if (TextUtils.isEmpty(filePath)) {
|
||||
return false;
|
||||
}
|
||||
for (String s : video_extension) {
|
||||
if (filePath.endsWith(s)) {
|
||||
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输入
|
||||
self.label_key = tk.Label(master, text="AES密钥 (Base64编码或16/24/32字节字符串):")
|
||||
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)
|
||||
|
||||
# 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.entry_iv = tk.Entry(master, width=50)
|
||||
self.entry_iv.pack(pady=5)
|
||||
@@ -64,21 +64,21 @@ class VideoEncryptorGUI:
|
||||
self.status_label.config(text="文件已选择,请设置密钥")
|
||||
|
||||
def generate_key_iv(self):
|
||||
"""生成随机的密钥和IV"""
|
||||
# 生成随机密钥(32字节,AES-256)和IV(16字节)
|
||||
"""生成随机的密钥和nonce"""
|
||||
# 生成随机密钥(32字节,AES-256)和nonce(8字节用于CTR模式)
|
||||
key = get_random_bytes(32)
|
||||
iv = get_random_bytes(16)
|
||||
nonce = get_random_bytes(8) # AES CTR模式使用的nonce应该是8字节
|
||||
|
||||
# 转换为Base64编码字符串,显示在输入框中
|
||||
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.insert(0, key_b64)
|
||||
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):
|
||||
"""加密视频文件"""
|
||||
@@ -94,35 +94,38 @@ class VideoEncryptorGUI:
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
# 处理密钥:尝试Base64解码,否则使用字符串编码
|
||||
try:
|
||||
key = base64.b64decode(key_str)
|
||||
except:
|
||||
key = key_str.encode('utf-8')
|
||||
|
||||
# try:
|
||||
# key = base64.b64decode(key_str)
|
||||
# except:
|
||||
key = key_str.encode('utf-8')
|
||||
|
||||
# 处理IV:尝试Base64解码,否则使用字符串编码
|
||||
try:
|
||||
iv = base64.b64decode(iv_str)
|
||||
except:
|
||||
iv = iv_str.encode('utf-8')
|
||||
|
||||
# try:
|
||||
# iv = base64.b64decode(iv_str)
|
||||
# except:
|
||||
iv = iv_str.encode('utf-8')
|
||||
|
||||
# 确保密钥长度为16、24或32字节
|
||||
if len(key) not in [16, 24, 32]:
|
||||
if len(key) < 16:
|
||||
key = key.ljust(16, b'\0')
|
||||
elif len(key) < 24:
|
||||
key = key.ljust(24, b'\0')
|
||||
elif len(key) < 32:
|
||||
key = key.ljust(32, b'\0')
|
||||
else:
|
||||
key = key[:32]
|
||||
# if len(key) not in [16, 24, 32]:
|
||||
# if len(key) < 16:
|
||||
# key = key.ljust(16, b'\0')
|
||||
# elif len(key) < 24:
|
||||
# key = key.ljust(24, b'\0')
|
||||
# elif len(key) < 32:
|
||||
# key = key.ljust(32, b'\0')
|
||||
# else:
|
||||
# key = key[:32]
|
||||
|
||||
# 确保IV长度为16字节
|
||||
if len(iv) != 16:
|
||||
if len(iv) < 16:
|
||||
iv = iv.ljust(16, b'\0')
|
||||
else:
|
||||
iv = iv[:16]
|
||||
# if len(iv) != 16:
|
||||
# if len(iv) < 16:
|
||||
# iv = iv.ljust(16, b'\0')
|
||||
# else:
|
||||
# iv = iv[:16]
|
||||
|
||||
# 生成输出文件路径
|
||||
file_dir = os.path.dirname(self.input_file_path)
|
||||
@@ -140,24 +143,30 @@ class VideoEncryptorGUI:
|
||||
messagebox.showerror("加密错误", f"加密过程中发生错误: {str(e)}")
|
||||
|
||||
def do_encryption(self, input_path, output_path, key, iv):
|
||||
print(f"iv len: {len(iv)}")
|
||||
|
||||
"""
|
||||
执行加密操作
|
||||
使用AES/CBC/PKCS5Padding模式,与Java代码保持一致
|
||||
使用AES/CTR/NoPadding模式
|
||||
"""
|
||||
# 读取输入文件
|
||||
with open(input_path, 'rb') as f_in:
|
||||
plaintext = f_in.read()
|
||||
|
||||
# 创建密码器
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
|
||||
# 应用PKCS5填充并加密
|
||||
padded_data = pad(plaintext, AES.block_size)
|
||||
ciphertext = cipher.encrypt(padded_data)
|
||||
|
||||
# 写入输出文件
|
||||
with open(output_path, 'wb') as f_out:
|
||||
f_out.write(ciphertext)
|
||||
try:
|
||||
# 创建CTR模式加密器
|
||||
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)
|
||||
|
||||
# 逐块加密文件
|
||||
while True:
|
||||
chunk = fin.read(4096) # 每次读取4KB
|
||||
if len(chunk) == 0:
|
||||
break
|
||||
encrypted_chunk = cipher.encrypt(chunk)
|
||||
fout.write(encrypted_chunk)
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"加密失败: {str(e)}")
|
||||
|
||||
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