如何在JS中使用SHA256对字符串进行哈希?

36

描述

我想在Javascript中使用SHA256本地哈希一个字符串。 我一直在寻找一些官方库或函数,但我发现很多不同的项目,每个项目都有不同的脚本,而且我不确定要信任哪些脚本(因为我不是专家,也绝对没有资格进行评估),也不知道如何实现它们。 编辑:我需要文本输出,而不是十六进制输出,很抱歉我在发布原始问题时没有解释清楚。

代码

这是我尝试过的内容:

async function sha256(message) {
  // encode as UTF-8
  const msgBuffer = new TextEncoder('utf-8').encode(message);

  // hash the message
  const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);

  // convert ArrayBuffer to Array
  const hashArray = Array.from(new Uint8Array(hashBuffer));

  // convert bytes to hex string
  const hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('');
  console.log(hashHex);
  return hashHex;
}
sha256(passwordInput); 

控制台输出:

未捕获(在Promise中)的TypeError:无法读取未定义的属性“digest”

我是javascript的新手,我欢迎所有建议,所以是的。

更新

尽管你们大多数人的建议都有效,但对于那些想使用Web Crypto API的人,答案在第5行。我需要将 crypto.subtle.digest更改为 window.crypto.subtle.digest


1
您使用的浏览器(包括版本)是什么? - user47589
@Amy 我正在使用 Google Chrome 版本 79.0.3945.130(64 位),希望这可以帮到你。 - Federico Fusco
你说得对 - 关于 window.crypto - 我已经将其作为答案添加了(在我注意到你在更新中写了同样的内容之前)。如果你仍然使用 StackOverflow,能否更新一下答案? - mikemaccana
请注意,如果您尝试对大文件(如1GB+)进行哈希处理,则crypto.subtle.digest不适用。目前,您必须求助于第三方库。 - jordanfb
这对我有用,但是我在处理 hashHex 返回值时遇到了问题,例如将其打印到屏幕上。我的解决方案是仅从控制台日志中检索哈希值 hashhex:对于本地使用,这是完全足够的。 - geralds
9个回答

59

你好:D,这是一个相当强大的功能。如果你是一名学者,你可能会喜欢查看这篇文章:https://www.movable-type.co.uk/scripts/sha256.html

Pure javascript:

var sha256 = function sha256(ascii) {
    function rightRotate(value, amount) {
        return (value>>>amount) | (value<<(32 - amount));
    };
    
    var mathPow = Math.pow;
    var maxWord = mathPow(2, 32);
    var lengthProperty = 'length'
    var i, j; // Used as a counter across the whole file
    var result = ''

    var words = [];
    var asciiBitLength = ascii[lengthProperty]*8;
    
    //* caching results is optional - remove/add slash from front of this line to toggle
    // Initial hash value: first 32 bits of the fractional parts of the square roots of the first 8 primes
    // (we actually calculate the first 64, but extra values are just ignored)
    var hash = sha256.h = sha256.h || [];
    // Round constants: first 32 bits of the fractional parts of the cube roots of the first 64 primes
    var k = sha256.k = sha256.k || [];
    var primeCounter = k[lengthProperty];
    /*/
    var hash = [], k = [];
    var primeCounter = 0;
    //*/

    var isComposite = {};
    for (var candidate = 2; primeCounter < 64; candidate++) {
        if (!isComposite[candidate]) {
            for (i = 0; i < 313; i += candidate) {
                isComposite[i] = candidate;
            }
            hash[primeCounter] = (mathPow(candidate, .5)*maxWord)|0;
            k[primeCounter++] = (mathPow(candidate, 1/3)*maxWord)|0;
        }
    }
    
    ascii += '\x80' // Append Ƈ' bit (plus zero padding)
    while (ascii[lengthProperty]%64 - 56) ascii += '\x00' // More zero padding
    for (i = 0; i < ascii[lengthProperty]; i++) {
        j = ascii.charCodeAt(i);
        if (j>>8) return; // ASCII check: only accept characters in range 0-255
        words[i>>2] |= j << ((3 - i)%4)*8;
    }
    words[words[lengthProperty]] = ((asciiBitLength/maxWord)|0);
    words[words[lengthProperty]] = (asciiBitLength)
    
    // process each chunk
    for (j = 0; j < words[lengthProperty];) {
        var w = words.slice(j, j += 16); // The message is expanded into 64 words as part of the iteration
        var oldHash = hash;
        // This is now the undefinedworking hash", often labelled as variables a...g
        // (we have to truncate as well, otherwise extra entries at the end accumulate
        hash = hash.slice(0, 8);
        
        for (i = 0; i < 64; i++) {
            var i2 = i + j;
            // Expand the message into 64 words
            // Used below if 
            var w15 = w[i - 15], w2 = w[i - 2];

            // Iterate
            var a = hash[0], e = hash[4];
            var temp1 = hash[7]
                + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) // S1
                + ((e&hash[5])^((~e)&hash[6])) // ch
                + k[i]
                // Expand the message schedule if needed
                + (w[i] = (i < 16) ? w[i] : (
                        w[i - 16]
                        + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15>>>3)) // s0
                        + w[i - 7]
                        + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2>>>10)) // s1
                    )|0
                );
            // This is only used once, so *could* be moved below, but it only saves 4 bytes and makes things unreadble
            var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) // S0
                + ((a&hash[1])^(a&hash[2])^(hash[1]&hash[2])); // maj
            
            hash = [(temp1 + temp2)|0].concat(hash); // We don't bother trimming off the extra ones, they're harmless as long as we're truncating when we do the slice()
            hash[4] = (hash[4] + temp1)|0;
        }
        
        for (i = 0; i < 8; i++) {
            hash[i] = (hash[i] + oldHash[i])|0;
        }
    }
    
    for (i = 0; i < 8; i++) {
        for (j = 3; j + 1; j--) {
            var b = (hash[i]>>(j*8))&255;
            result += ((b < 16) ? 0 : '') + b.toString(16);
        }
    }
    return result;
};

Source: https://geraintluff.github.io/sha256/


4
我不明白为什么人们要给你的投票点踩?你的函数可以运行,并且没有依赖关系。 - Albert Renshaw
18
我没有点踩,但我认为他被点踩是因为在密码学领域,强烈建议不要自己编写加密算法,因为这些算法没有经过审查,可能存在安全问题。通常最好选择一个知名的库,它可以提供最佳的安全性以及通常的更新。 - Mr_Antivius
10
我认为这对于教育目的非常好,非常感谢。 - David ROUMANET
17
因为从Stack Overflow复制粘贴代码是一种依赖关系。复制的代码只是一个未受管理的依赖项,很少有错误报告和没有集中的更新机制。这些代码肯定对教育目的有好处,但我担心把复制粘贴代码作为使用库的替代品来呈现。 - mikemaccana
1
我没有使用这个答案,但它指引了我正确的方向。这是一些我在此基础上找到的其他信息:Web 加密 API:https://developer.mozilla.org/zh-CN/docs/Web/API/SubtleCrypto/digest - carloswm85
显示剩余2条评论

54

2021更新 - SHA256现已包含在当前浏览器中

正如您在问题中提到的,您不需要自定义加密实现来完成此操作。

WebCrypto在所有当前浏览器中都受支持。使用window.crypto.subtle.digest进行SHA 256哈希计算。

'digest'是一个稍微过时的术语,用来指代哈希。人们过去常常将哈希函数称为'message digests'(消息摘要)-有些人仍然如此。

基于MDN示例,我已在npm上发布了这段代码

npm i boring-webcrypto-sha256

那么

import { getSHA256Hash } from "boring-webcrypto-sha256";

或者如果你想保留自己的版本:
const getSHA256Hash = async (input) => {
  const textAsBuffer = new TextEncoder().encode(input);
  const hashBuffer = await window.crypto.subtle.digest("SHA-256", textAsBuffer);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hash = hashArray
    .map((item) => item.toString(16).padStart(2, "0"))
    .join("");
  return hash;
};

然后只需使用 await getSHA256Hash('someInput')

您可以使用 Linux 命令行来确认这是正确的

echo -n 'someInput' | sha256sum

这将给你相同的哈希值。

3
请注意,在Chrome 60中,他们添加了一个功能,禁用非TLS连接的crypto.subtle。请参阅https://dev59.com/xqXja4cB1Zd3GeqPXddB#46671627。 - Ser
6
"Native is async too await crypto.subtle.digest(). Returns a Promise (see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#return_value)"翻译:await crypto.subtle.digest() 也是异步的。返回一个 Promise 对象(请参见 https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#return_value)。 - jordanfb
注意,数据不是字符串,返回的数据也不是字符串(它是一个 Promise)。要进行转换和异步操作,请使用 Mozilla 链接中的示例。 - djdance
@djdance 如果你的意思是你有一个 Promise 并且想要获取 Promise 内部的值,那么你需要使用 await - mikemaccana

6

看这里:https://github.com/brix/crypto-js

你可以使用以下内容:

require(["crypto-js/aes", "crypto-js/sha256"], function (AES, SHA256)
{
    console.log(SHA256("Message")); 
});

或者不使用 require:

<script type="text/javascript" src="path-to/bower_components/crypto-js/crypto-js.js"></script>
<script type="text/javascript">
    var encrypted = CryptoJS.AES(...);
    var encrypted = CryptoJS.SHA256(...);
</script>

谢谢你的回答(它有效),但我需要输出以文本格式而不是十六进制格式呈现,如果在问题描述中没有表述清楚,我很抱歉。 - Federico Fusco
我认为这是一种文本格式(它看起来像十六进制)。 - Gen4ik

4

纯JavaScript,不需要依赖

您可以使用SubtleCrypto.digest()来帮助您。

它需要一个Uint8Array

如果您的数据是一个Blob

const blob = new Blob([file])
const arrayBuffer = await blob.arrayBuffer()
const uint8Array = new Uint8Array(arrayBuffer)
SubtleCrypto.digest("SHA-256", uint8Array)

如果数据是字符串,则使用TextEncoder.encode()将其转换为Uint8Array。
const uint8Array = new TextEncoder().encode(data)
SubtleCrypto.digest("SHA-256", uint8Array)

以下是一个可运行的示例供您参考。

<input type="file" multiple/>
<input placeholder="Press `Enter` when done."/>
<script>

  /**
   * @param {"SHA-1"|"SHA-256"|"SHA-384"|"SHA-512"} algorithm https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
   * @param {string|Blob} data
   */
  async function getHash(algorithm, data) {

    const main = async (msgUint8) => { // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
      const hashBuffer = await crypto.subtle.digest(algorithm, msgUint8)
      const hashArray = Array.from(new Uint8Array(hashBuffer))
      return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
    }

    if (data instanceof Blob) {
      const arrayBuffer = await data.arrayBuffer()
      const msgUint8 = new Uint8Array(arrayBuffer)
      return await main(msgUint8)
    }
    const encoder = new TextEncoder()
    const msgUint8 = encoder.encode(data)
    return await main(msgUint8)
  }

  const inputFile = document.querySelector(`input[type="file"]`)
  const inputText = document.querySelector(`input[placeholder^="Press"]`)
  inputFile.onchange = async (event) => {
    for (const file of event.target.files) {
      console.log(file.name, file.type, file.size + "bytes")
      const hashHex = await getHash("SHA-256", new Blob([file]))
      console.log(hashHex)
    }
  }

  inputText.onkeyup = async (keyboardEvent) => {
    if (keyboardEvent.key === "Enter") {
      const hashHex = await getHash("SHA-256", keyboardEvent.target.value)
      console.log(hashHex)
    }
  }
</script>


这是我两个月前的回答副本。 - mikemaccana
我不认为存在抄袭的嫌疑,主要方法是相同的,但我尝试让人们理解参数的含义并提供可运行的示例。我的分数没有因此而下降,所以我认为你还是很有礼貌的,因为你只表达了个人观点。我的母语不是英语,我理解你的想法是文档提供了足够的信息,但对我来说最快的理解方式是有一个实际的例子,这就是我发布答案的原因。 - Carson
@mikemaccana:所以这正是SO想让我们做的事情:从原始来源复制信息,因为我们想在这里堆积信息(这使它成为一个未受管理的依赖项,但yolo)。 - Fred
@Carson 很抱歉我在用语上应该更加精确。我并不认为你剽窃了任何内容,我只是说你的回答与现有的一个答案重复了。 - mikemaccana

2
调用 crypto.subtle.digest 时出现 Cannot read property 'digest' of undefined 的错误,意味着在 crypto 中没有可用的 subtle;因此,digest 不可能存在,因为它所在的模块也不存在。
因此,可以推断在此范围内 crypto.subtle 不可用,在浏览器中除了安全上下文之外的任何地方都是如此。

何时被认为是安全上下文?developer.mozilla.org

当上下文经过安全传递(或本地传递)且不能用于向非安全上下文提供访问安全 API 的情况下,上下文将被视为安全。实际上,这意味着对于一个页面来说,它和所有其父级和打开者链上的页面都必须通过安全传递进行传递。

例如,通过 TLS 安全传输的页面如果有未经安全传递的父级或祖先文档,则不被视为安全上下文;否则该页面将能够通过 postMessage 消息向非安全传递的祖先公开敏感 API。同样地,如果一个通过 TLS 传递的文档由不安全上下文在未指定 noopener 的情况下在新窗口中打开,则该打开的窗口不被视为安全上下文(因为打开者和打开的窗口可以通过 postMessage 进行通信)。

本地传递的文件,例如 http:// localhost* 和 file:// 路径,被认为是经过安全传递的。

¦¦¦¦¦ 不是本地的上下文必须通过 https://wss:// 进行服务,而且使用的协议不应被视为已废弃。

在安全上下文中,您的代码可以完美运行。 1: 安全上下文 - Web 安全 | MDN
2: 何时才能认为一个上下文是安全的?- 安全上下文 - Web 安全 | MDN
3: Window.postMessage() - Web API | MDN
4: Window 功能特性 - Window.open() - Web API | MDN

1
代码没问题。 最后一行应该是这样的:
var vDigest = await sha256(passwordInput);

1

快速回答:

const sha256 = async (data) => {
    const textAsBuffer = new TextEncoder().encode(data);
    const hashBuffer = await window.crypto.subtle.digest('SHA-256', textAsBuffer);
    const hashArray = Array.from(new Uint8Array(hashBuffer))
    const digest = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    }
sha256("data")

0

请参考如何在浏览器JavaScript中计算字符串的SHA哈希值

您可以使用CDN库。

https://cdnjs.com/libraries/crypto-js

请将以下脚本标记引用到您的HTML中:
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js" integrity="sha512-E8QSvWZ0eCLGk4km3hxSsNmGWbLtSCSUcewDQPQWZF6pEU8GlT8a5fF32wOl1i8ftdMhssTrF/OhyGWwonTcXA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

如何使用库中的方法,请参考下面的网站:

https://cryptojs.gitbook.io/docs/

下面的页面展示了一个例子,说明了我的建议:

https://codepen.io/omararcus/pen/QWwBdmo


虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅有链接的答案可能会失效。-【来自审查】 - Nick Vu

0
上面的答案来自ofundefined,其中有许多缺少分号和错误。我已经清理了代码,以便您可以将其作为函数调用。我不确定这是否适用于Unicode字符。可能必须将Unicode转换为普通ASCII才能正常工作。但是,正如链接的文章所说...这不应该在生产环境中使用。此外,似乎原始版本的此代码确实支持Unicode,因此也许最好使用那个而不是这个函数。
上面的文章(似乎支持Unicode)显示了与下面发布的函数不同的函数...

https://www.movable-type.co.uk/scripts/sha256.html

如何使用

console.log(sha256('Perry Computer Services'));

输出

89bae4aeb761e42cb71ba6b62305e0980154cf21992c9ab2ab6fc40966ab5bf3

使用 PHP HASH 进行测试 - 输出结果

89bae4aeb761e42cb71ba6b62305e0980154cf21992c9ab2ab6fc40966ab5bf3

这不是完整的函数,如上面链接的页面所示。它可以与非Unicode字符一起“按原样”工作,正如您可以从我的上面的示例中看到的那样。

function sha256(ascii) {
    function rightRotate(value, amount) {
        return (value >>> amount) | (value << (32 - amount));
    }
    ;

    var mathPow = Math.pow;
    var maxWord = mathPow(2, 32);
    var lengthProperty = 'length';
    var i, j; // Used as a counter across the whole file
    var result = '';

    var words = [];
    var asciiBitLength = ascii[lengthProperty] * 8;

    //* caching results is optional - remove/add slash from front of this line to toggle
    // Initial hash value: first 32 bits of the fractional parts of the square roots of the first 8 primes
    // (we actually calculate the first 64, but extra values are just ignored)
    var hash = sha256.h = sha256.h || [];
    // Round constants: first 32 bits of the fractional parts of the cube roots of the first 64 primes
    var k = sha256.k = sha256.k || [];
    var primeCounter = k[lengthProperty];
    /*/
     var hash = [], k = [];
     var primeCounter = 0;
     //*/

    var isComposite = {};
    for (var candidate = 2; primeCounter < 64; candidate++) {
        if (!isComposite[candidate]) {
            for (i = 0; i < 313; i += candidate) {
                isComposite[i] = candidate;
            }
            hash[primeCounter] = (mathPow(candidate, .5) * maxWord) | 0;
            k[primeCounter++] = (mathPow(candidate, 1 / 3) * maxWord) | 0;
        }
    }

    ascii += '\x80'; // Append Ƈ' bit (plus zero padding)
    while (ascii[lengthProperty] % 64 - 56)
        ascii += '\x00'; // More zero padding

    for (i = 0; i < ascii[lengthProperty]; i++) {
        j = ascii.charCodeAt(i);
        if (j >> 8)
            return; // ASCII check: only accept characters in range 0-255
        words[i >> 2] |= j << ((3 - i) % 4) * 8;
    }
    words[words[lengthProperty]] = ((asciiBitLength / maxWord) | 0);
    words[words[lengthProperty]] = (asciiBitLength);

    // process each chunk
    for (j = 0; j < words[lengthProperty]; ) {
        var w = words.slice(j, j += 16); // The message is expanded into 64 words as part of the iteration
        var oldHash = hash;
        // This is now the undefinedworking hash", often labelled as variables a...g
        // (we have to truncate as well, otherwise extra entries at the end accumulate
        hash = hash.slice(0, 8);

        for (i = 0; i < 64; i++) {
            var i2 = i + j;
            // Expand the message into 64 words
            // Used below if 
            var w15 = w[i - 15], w2 = w[i - 2];

            // Iterate
            var a = hash[0], e = hash[4];
            var temp1 = hash[7]
                    + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) // S1
                    + ((e & hash[5]) ^ ((~e) & hash[6])) // ch
                    + k[i]
                    // Expand the message schedule if needed
                    + (w[i] = (i < 16) ? w[i] : (
                            w[i - 16]
                            + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15 >>> 3)) // s0
                            + w[i - 7]
                            + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10)) // s1
                            ) | 0
                            );
            // This is only used once, so *could* be moved below, but it only saves 4 bytes and makes things unreadble
            var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) // S0
                    + ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2])); // maj

            hash = [(temp1 + temp2) | 0].concat(hash); // We don't bother trimming off the extra ones, they're harmless as long as we're truncating when we do the slice()
            hash[4] = (hash[4] + temp1) | 0;
        }

        for (i = 0; i < 8; i++) {
            hash[i] = (hash[i] + oldHash[i]) | 0;
        }
    }

    for (i = 0; i < 8; i++) {
        for (j = 3; j + 1; j--) {
            var b = (hash[i] >> (j * 8)) & 255;
            result += ((b < 16) ? 0 : '') + b.toString(16);
        }
    }
    return result;
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接