【问题标题】:Issues with SHA1 hash implementation in AndroidAndroid 中 SHA1 哈希实现的问题
【发布时间】:2011-09-15 02:12:39
【问题描述】:

我有两个小 sn-ps 用于计算 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();
}

我希望有另一种可能让它运行,因为我需要性能。

【问题讨论】:

    标签: java android hash stream sha1


    【解决方案1】:

    我使用了一个用 JNI 加载的高性能 c++ 实现。
    更多详情请在评论中留言。

    编辑:
    JNI 的要求是Android NDK。对于 Windows,还需要 cygwin 或类似的东西。
    如果你决定使用 cygwin,我会给你一些关于如何让它与 NDK 一起工作的小说明:

    1. 从 cygwin 下载 setup.exe 并执行。
    2. 点击下一步并选择从互联网安装确认下一步
    3. 接下来的两个步骤根据需要调整设置,并一如既往地单击下一步
    4. 选择您的互联网连接和与最后阶段相同的过程。
    5. 一个下载页面会吸引眼球,选择它或只选择一个下载页面,该页面位于您所在的国家/地区。没什么好说的了。
    6. 我们需要包 makegcc-g++。您可以使用左上角的搜索找到它们,单击Skip 直到显示版本并选择第一个字段。做我们在选择后一直做的事情。
    7. 您将获得信息,即存在必须解决的依赖关系。一般不需要自己做和确认。
    8. 下载和安装开始。
    9. 如果您需要,您可以创建快捷方式,否则请点击特殊的完成
    10. 下载 zip 文件并将 NDK 解压缩到不包含空格的路径。
    11. 您现在可以开始 cygwin。
    12. 导航到 NDK。路径 /cydrive 为您提供所有可用的驱动器 f.e. cd /cygdrive/d 导航到带有字母 D 的驱动器。
    13. 在 NDK 的根文件夹中,您可以使用 ./ndk-build 执行文件 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 代码:
    我使用带有 ProgressDialogAsyncTask 来向用户提供有关操作的一些反馈。

    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 &lt;jni.h&gt;

    对于记录插入,包括#include &lt;android/log.h&gt;
    现在您可以使用__android_log_print(ANDROID_LOG_DEBUG, DEBUG_TAG, "Version [%s]", "19"); 登录。
    第一个参数是消息的类型,第二个参数是导致库。
    你可以看到我的代码中有一个版本号。这非常有用,因为有时 apk 构建器不使用新的本机库。如果在线版本错误,故障排除可以大大缩短。

    本机代码中的命名约定有点粗俗:Java_[package name]_[class name]_[method name]

    总是给出第一个参数,但根据应用程序,您应该区分:

    • 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 &lt;path_to_the_used_android_api&gt; 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-&gt;FindClass(CALL_CLASS);。在这种情况下,CALL_CLASS 是类 de/dhbw/file/sha1/SHA1HashFileAsyncTask 的完整限定路径。

    要查找方法,您需要 JNIEnv 和类的实例:
    jmethodID midSet = callEnv-&gt;GetMethodID(callClass, "setFileSize", "(J)V"); 第一个参数是类的实例,第二个是方法的名称,第三个是方法的签名。
    您可以使用从 JDK 给定的二进制 javap.exe 获得的签名。只需使用类 f.e. 的完整路径调用它即可。 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-build,它位于 NDK 根目录中。
    这将开始编译,如果成功,您将得到如下输出:

    $ /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 功能 clean project 是有利的。

    调试
    java代码的调试和以前没有什么不同。
    c++代码的调试将在下期进行。

    【讨论】:

    • 您能否详细说明如何执行此操作?
    【解决方案2】:

    这样做:

    MessageDigest md = MessageDigest.getInstance("SHA1");
    InputStream in = new FileInputStream("hereyourinputfilename");
    byte[] buf = new byte[8192];
    for (;;) {
        int len = in.read(buf);
        if (len < 0)
            break;
        md.update(buf, 0, len);
    }
    in.close();
    byte[] hash = md.digest();
    

    性能来自于按块处理数据。像这里一样,一个 8 kB 的缓冲区应该足够块状。您不必使用BufferedInputStream,因为 8 kB 缓冲区也可用作 I/O 缓冲区。

    【讨论】:

    • 在我的电脑上速度很快,但在我的安卓设备上却不行。
    • @H3llGhost:您得到的MessageDigest 实例很可能是用纯Java 编写的,而Java 在Android 2.1 设备上在计算密集型任务时速度很慢。据称 2.2 及更高版本的情况要好得多(JVM 然后有一个 JIT 编译器)。您无能为力。
    • 感谢您的建议。我已经在使用 2.2。也许我应该寻找基于 c++ 并由 JNI 调用的 sha1 函数。
    【解决方案3】:

    速度快且不正确的原因是(我认为)它没有对文件内容进行哈希处理!

    FileInputStream fis = new FileInputStream("C:/Users/Ich/Downloads/srware_iron.exe");
    ByteArrayInputStream byteArrayInputStream = 
            new ByteArrayInputStream(fis.toString().getBytes());
    

    fis.toString() 调用不会读取文件的内容。相反,它为您提供了一个(我怀疑)看起来像这样的字符串:

    "java.io.FileInputStream@xxxxxxxx"
    

    然后您将继续为其计算 SHA1 哈希。 FileInputStream 及其超类不会覆盖 Object::toString ...


    将 InputStream 的全部内容读取到 byte[] 的简单方法是使用 Apache Commons I/O 辅助方法 - IOUtils.toByteArray(InputStream)

    【讨论】:

      【解决方案4】:
          public void computeSHAHash(String path)// path to your file
          {
                  String SHAHash = null;
          try 
          {
              MessageDigest md = MessageDigest.getInstance("SHA1");
              InputStream in = new FileInputStream(path);
              byte[] buf = new byte[8192];
              int len = -1;
              while((len = in.read(buf)) > 0) 
              {
                  md.update(buf, 0, len);
              }
              in.close();
              byte[] data = md.digest();
              try 
              {
                 SHAHash = convertToHex(data);
              } 
              catch (IOException e) 
              {
                 // TODO Auto-generated catch block
                 e.printStackTrace();
              }
          } catch (NoSuchAlgorithmException e) {
              // TODO Auto-generated catch block
              e.printStackTrace();
          } catch (FileNotFoundException e) {
              // TODO Auto-generated catch block
              e.printStackTrace();
          } catch (IOException e) {
              // TODO Auto-generated catch block
              e.printStackTrace();
          }
            Toast.makeToast(getApplicationContext(),"Generated Hash ="+SHAHash,Toast.LENGTH_SHORT).show();  
      
         }
       private static String convertToHex(byte[] data) throws java.io.IOException
      {
          StringBuffer sb = new StringBuffer();
          String hex = null;
      
          hex = Base64.encodeToString(data, 0, data.length, NO_OPTIONS);
      
          sb.append(hex);
      
          return sb.toString();
      }
      

      【讨论】:

      • convertToHex 这里是完全错误的。 Base64 不是十六进制的。十六进制以 16 为基数。
      猜你喜欢
      • 1970-01-01
      • 2021-04-11
      • 1970-01-01
      • 1970-01-01
      • 2020-03-04
      • 1970-01-01
      • 2012-09-26
      • 2011-07-24
      • 1970-01-01
      相关资源
      最近更新 更多