Android中SHA1哈希实现的问题
问题内容:
我有两个用于计算SHA1的小片段。
一个非常快,但似乎不正确,另一个非常慢,但正确。
我认为FileInputStream
转换为ByteArrayInputStream
问题。
快速版本:
MessageDigest md = MessageDigest.getInstance("SHA1");
FileInputStream fis = new FileInputStream("path/to/file.exe");
ByteArrayInputStream byteArrayInputStream =
new ByteArrayInputStream(fis.toString().getBytes());
DigestInputStream dis = new DigestInputStream(byteArrayInputStream, md);
BufferedInputStream bis = new BufferedInputStream(fis);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int ch;
while ((ch = dis.read()) != -1) {
byteArrayOutputStream.write(ch);
}
byte[] newInput = byteArrayOutputStream.toByteArray();
System.out.println("in digest : " +
byteArray2Hex(dis.getMessageDigest().digest()));
byteArrayOutputStream = new ByteArrayOutputStream();
DigestOutputStream digestOutputStream =
new DigestOutputStream(byteArrayOutputStream, md);
digestOutputStream.write(newInput);
System.out.println("out digest: " +
byteArray2Hex(digestOutputStream.getMessageDigest().digest()));
System.out.println("length: " +
new String(
byteArray2Hex(digestOutputStream.getMessageDigest().digest())).length());
digestOutputStream.close();
byteArrayOutputStream.close();
dis.close();
慢版本:
MessageDigest algorithm = MessageDigest.getInstance("SHA1");
FileInputStream fis = new FileInputStream("path/to/file.exe");
BufferedInputStream bis = new BufferedInputStream(fis);
DigestInputStream dis = new DigestInputStream(bis, algorithm);
// read the file and update the hash calculation
while (dis.read() != -1);
// get the hash value as byte array
byte[] hash = algorithm.digest();
转换方式:
private static String byteArray2Hex(byte[] hash) {
Formatter formatter = new Formatter();
for (byte b : hash) {
formatter.format("%02x", b);
}
return formatter.toString();
}
我希望有另一种可能使其运行,因为我需要性能。
问题答案:
我使用了JNI加载的高性能c ++实现。
有关更多详细信息,请发表评论。
编辑:
JNI的要求是Android
NDK。对于Windows,还需要cygwin或类似的东西。
如果您决定使用cygwin,我会给您一些指导,说明如何使其与NDK一起使用:
- 从cygwin 下载 setup.exe 并执行。
- 点击 下一步 ,并选择 从网络安装 确认有 下一步 。
- 接下来的两个步骤将根据需要调整设置,并一如既往地单击“ 下一步” 。
- 选择您的互联网连接,并按照与最后阶段相同的步骤进行操作。
- 选择一个下载页面会吸引您的注意,或者仅下载您所在国家/地区的下载页面。没什么可说的了。
- 我们需要软件包 make 和 gcc-g ++ 。您可以使用左上角的搜索找到它们,单击“ 跳过 直到显示版本” ,然后选择第一个字段。完成选择后我们一直做的事情。
- 您将获得必须解决的依赖关系信息。通常无需自己进行确认。
- 开始下载和安装。
- 如果需要,您可以创建快捷方式,否则单击特殊的 Finish 。
- 下载该zip文件,然后将NDK解压缩到不包含路径的路径中。
- 您可以立即开始cygwin。
- 导航到NDK。路径 / cydrive 为您提供所有可用的驱动器,例如
cd /cygdrive/d
导航到字母 D 的驱动器。 - 在NDK的根文件夹,您可以执行该文件 NDK建造 用
./ndk-build
。应该出现类似的错误Android NDK: Could not find application project directory !
。
您必须在Android项目中导航才能执行命令。因此,让我们从一个项目开始。
在我们从项目开始之前,搜索哈希算法的C / C
++实现。我从此站点CSHA1中获取了代码。
您应该根据需要编辑源代码。
现在我们可以从JNI开始。
您在Android项目中创建一个名为 jni 的文件夹。它还包含所有本机源文件和 Android.mk (稍后将进一步介绍该文件)。
将下载的(和编辑的)源文件复制到该文件夹中。
我的java包称为 de.dhbw.file.sha1 ,因此我将源文件命名为类似文件,以便轻松找到它们。
Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_LDLIBS := -llog
# How the lib is called?
LOCAL_MODULE := SHA1Calc
# Which is your main SOURCE(!) file?
LOCAL_SRC_FILES := de_dhbw_file_sha1_SHA1Calc.cpp
include $(BUILD_SHARED_LIBRARY)
Java代码:
我将 AsyncTask 与 ProgressDialog结合使用, 以向用户提供有关该操作的一些反馈。
package de.dhbw.file.sha1;
// TODO: Add imports
public class SHA1HashFileAsyncTask extends AsyncTask<String, Integer, String> {
// [...]
static {
// loads a native library
System.loadLibrary("SHA1Calc");
}
// [...]
// native is the indicator for native written methods
protected native void calcFileSha1(String filePath);
protected native int getProgress();
protected native void unlockMutex();
protected native String getHash();
// [...]
}
本机代码(C ++):
请记住,访问本机代码内部的变量或使用线程的其他方式需要同步,否则您很快会遇到分段错误!
要使用JNI,您必须添加#include <jni.h>
。
对于日志记录,请插入include #include <android/log.h>
。
现在您可以使用登录__android_log_print(ANDROID_LOG_DEBUG, DEBUG_TAG, "Version [%s]", "19");
。
第一个参数是消息的类型,第二个参数是原因库。
您可以看到我的代码中有一个版本号。这很有用,因为有时apk生成器不使用新的本机库。如果在线版本错误,则可以极大地缩短故障排除时间。
本机代码中的命名约定有点麻烦:Java_[package name]_[class name]_[method name]
。
始终提供first到arguments,但是应根据应用程序进行区分:
func(JNIEnv * env, jobject jobj)
-> JNI调用是一个实例方法func(JNIEnv * env, jclass jclazz)
-> JNI调用是静态方法
方法的标头calcFileSha1(...)
:
JNIEXPORT void JNICALL Java_de_dhbw_file_sha1_SHA1HashFileAsyncTask_calcFileSha1(JNIEnv * env, jobject jobj, jstring file)
JDK提供了二进制 javah.exe ,该文件生成了本机代码的头文件。用法非常简单,只需使用完全合格的类即可调用它:
javah de.dhbw.file.sha1.SHA1HashFileAsyncTask
在我的情况下,我必须额外提供 bootclasspath ,因为我使用的是Android类: javah -bootclasspath <path_to_the_used_android_api> de.dhbw.file.sha1.SHA1HashFileAsyncTask
那将是生成的文件:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class de_dhbw_file_sha1_SHA1HashFileAsyncTask */
#ifndef _Included_de_dhbw_file_sha1_SHA1HashFileAsyncTask
#define _Included_de_dhbw_file_sha1_SHA1HashFileAsyncTask
#ifdef __cplusplus
extern "C" {
#endif
#undef de_dhbw_file_sha1_SHA1HashFileAsyncTask_ERROR_CODE
#define de_dhbw_file_sha1_SHA1HashFileAsyncTask_ERROR_CODE -1L
#undef de_dhbw_file_sha1_SHA1HashFileAsyncTask_PROGRESS_CODE
#define de_dhbw_file_sha1_SHA1HashFileAsyncTask_PROGRESS_CODE 1L
/*
* Class: de_dhbw_file_sha1_SHA1HashFileAsyncTask
* Method: calcFileSha1
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_de_dhbw_file_sha1_SHA1HashFileAsyncTask_calcFileSha1
(JNIEnv *, jobject, jstring);
/*
* Class: de_dhbw_file_sha1_SHA1HashFileAsyncTask
* Method: getProgress
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_de_dhbw_file_sha1_SHA1HashFileAsyncTask_getProgress
(JNIEnv *, jobject);
/*
* Class: de_dhbw_file_sha1_SHA1HashFileAsyncTask
* Method: unlockMutex
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_de_dhbw_file_sha1_SHA1HashFileAsyncTask_unlockMutex
(JNIEnv *, jobject);
/*
* Class: de_dhbw_file_sha1_SHA1HashFileAsyncTask
* Method: getHash
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_de_dhbw_file_sha1_SHA1HashFileAsyncTask_getHash
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
您可以更改文件而无需另行通知。 但是不要再使用javah
!
类和方法
要获取类实例,可以使用jclass clz = callEnv->FindClass(CALL_CLASS);
。在这种情况下,是CALL_CLASS
类 de / dhbw / file /
sha1 / SHA1HashFileAsyncTask 的完整限定路径。
要查找方法,您需要 JNIEnv 和该类的实例:
jmethodID midSet = callEnv->GetMethodID(callClass, "setFileSize", "(J)V");
第一个参数是该类的实例,第二个参数是该方法的名称,第三个是该方法的签名。
您可以从JDK给定的二进制 javap.exe中 获得签名。只需使用fe类的完全限定路径进行调用即可javap -s de.dhbw.file.sha1.SHA1HashFileAsyncTask
。
您将得到如下结果:
Compiled from "SHA1HashFileAsyncTask.java"
public class de.dhbw.file.sha1.SHA1HashFileAsyncTask extends android.os.AsyncTas
k<java.lang.String, java.lang.Integer, java.lang.String> {
[...]
static {};
Signature: ()V
public de.dhbw.file.sha1.SHA1HashFileAsyncTask(android.content.Context, de.dhb
w.file.sha1.SHA1HashFileAsyncTask$SHA1AsyncTaskListener);
Signature: (Landroid/content/Context;Lde/dhbw/file/sha1/SHA1HashFileAsyncTas
k$SHA1AsyncTaskListener;)V
protected native void calcFileSha1(java.lang.String);
Signature: (Ljava/lang/String;)V
protected native int getProgress();
Signature: ()I
protected native void unlockMutex();
Signature: ()V
protected native java.lang.String getHash();
Signature: ()Ljava/lang/String;
[...]
public void setFileSize(long);
Signature: (J)V
[...]
}
如果找到该方法,则变量不等于0。
调用该方法非常容易:
callEnv->CallVoidMethod(callObj, midSet, size);
第一个参数是“ main”方法给定的 jobject ,我认为其他方法很明确。
请记住,尽管本类是私有方法,但您可以从本机代码调用,因为本机代码是其中的一部分!
字符串
给定的字符串将与下面的代码转换:
jboolean jbol;
const char *fileName = env->GetStringUTFChars(file, &jbol);
另一种方式:
TCHAR* szReport = new TCHAR;
jstring result = callEnv->NewStringUTF(szReport);
它可以是每个char*
变量。
例外情况
可与抛出 的JNIEnv :
callEnv->ThrowNew(callEnv->FindClass("java/lang/Exception"),
"Hash generation failed");
您还可以检查 JNIEnv 是否也发生了异常:
if (callEnv->ExceptionOccurred()) {
callEnv->ExceptionDescribe();
callEnv->ExceptionClear();
}
技术指标
建立/清洁
生成
创建完所有文件并将其填充内容之后,就可以对其进行生成。
打开cygwin,导航到项目根目录,然后从NDK根目录中执行 ndk-build 。
这开始编译,如果成功,您将得到如下输出:
$ /cygdrive/d/android-ndk-r5c/ndk-build
Compile++ thumb : SHA1Calc <= SHA1Calc.cpp
SharedLibrary : libSHA1Calc.so
Install : libSHA1Calc.so => libs/armeabi/libSHA1Calc.so
如果有任何错误,您将从编译器获取典型输出。
清洁
打开cygwin,打开您的Android项目并执行命令/cygdrive/d/android-ndk-r5c/ndk-build clean
。
生成apk
在构建本机库之后,您可以构建您的项目。我发现干净了,使用eclipse功能 清理项目 是有利的。
调试
Java代码的调试与以前没有什么不同。
C ++代码的调试将在下一次进行。