2021 update - SHA256 is now included in current browsers
As you mention in your question, you don't need custom Crypto implementations to do this.
WebCrypto is supported in all current browsers. Use window.crypto.subtle.digest to make a SHA 256 hash.
'digest' by the way is a slightly dated way to refer to a hash. People used to refer to hashing functions as 'message digests' - some people still do.
Based on MDN example, I've published this code on npm:
npm i boring-webcrypto-sha256
Then
import { getSHA256Hash } from "boring-webcrypto-sha256";
Or if you want to maintain your own version:
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;
};
Then just use await getSHA256Hash('someInput')
You can confirm this is correct using the Linux command line:
echo -n 'someInput' | sha256sum
Which will give you the same hash.
Answer from mikemaccana on Stack Overflow2021 update - SHA256 is now included in current browsers
As you mention in your question, you don't need custom Crypto implementations to do this.
WebCrypto is supported in all current browsers. Use window.crypto.subtle.digest to make a SHA 256 hash.
'digest' by the way is a slightly dated way to refer to a hash. People used to refer to hashing functions as 'message digests' - some people still do.
Based on MDN example, I've published this code on npm:
npm i boring-webcrypto-sha256
Then
import { getSHA256Hash } from "boring-webcrypto-sha256";
Or if you want to maintain your own version:
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;
};
Then just use await getSHA256Hash('someInput')
You can confirm this is correct using the Linux command line:
echo -n 'someInput' | sha256sum
Which will give you the same hash.
Hellow there :D it's quite a function. If you are a scholar, you would like to check this article: 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/
On https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest I found this snippet that uses internal js module:
async function sha256(message) {
// encode as UTF-8
const msgBuffer = new TextEncoder().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 => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
Note that crypto.subtle in only available on https or localhost - for example for your local development with python3 -m http.server you need to add this line to your /etc/hosts:
0.0.0.0 localhost
Reboot - and you can open localhost:8000 with working crypto.subtle.
Most modern browsers now support crypto operations natively. See https://developer.mozilla.org/en-US/docs/Web/API/Crypto.
That said, client-side crypto is usually not as good an idea as it might seem.
The fundamental principle is that you (at the server end) have to assume that everything happening on the client side may be malicious, and may not even be running your code at all. If your crypto operations are meant to protect the user's data from a third-party's discovery or interference, the appropriate crypto layer to use is TLS rather than rolling your own in the client.
Sending sensitive data over TLS does not require additional crypto in transit beyond that provided transparently by TLS for compliance with any laws or best practices. If you're sending sensitive data, send it over TLS. From your application's perspective this will look like plain-text, but that's entirely appropriate and merely a reflection of the level of abstraction of the encryption, not the level of security it provides. Just because a security mechanism feels simple to use doesn't mean the protections it provides are simplistic.
And don't say: "I'm already using TLS, but I'm making it BETTER by doing client-side as well." Complicating your application with more security-adjacent components doesn't add security. Instead, you're sacrificing true long-term security in order to feel like you're doing more.
The context specified for this question is exactly the wrong place to do client-side crypto. There's a good reason why major tech companies never do what this question suggests, and they're successfully fending off nation-state level attackers.
The case where client-side browser crypto is appropriate is when the client is considered stand-alone and the user's protection is not expected to be guarded by the server operator. This is an exceptionally rare situation, but shows up in things like offline web applications.
Videos
» npm install js-sha256
Using 62 characters seems an odd choice:
const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
Prefer base64url.
For one thing, the various divide and modulo base operations
become simple bit shifts when length of digits is 2^6.
For another, it becomes easier to predict how encoded message length relates to original length.
result = result * base + digits.indexOf(string.charAt(i));
The repeated indexOf O(N) linear scan seems tedious. Prefer to use a hash map which gives the answer in O(1) constant time.
while (0 < int) {
Clearly this is identical to an int > 0 expression.
Except that the Gentle Reader can pronounce the latter
as "while int positive".
As written it seems a little contorted to make sense of it.
const a = ...
const h = bits[i + 7];
I don't get it.
Why introduce a bunch of temp vars?
Just console.log eight expressions
and be done with it.
Or iterate through a loop eight times.
This sha_256 function seems to be
doing at least two things, computing
a hash and sending it to the console.
The single responsibility
principle suggests breaking this out into
two functions.
This submission is crying out for a couple of simple unit tests. It's sooo easy. Just add one or two, so we can see what an encode / decode roundtrip looks like.
wanted to know whether an approach like this is secure
That depends on your use case, which you did not describe. The good news is you're starting with a cryptographic hash function so an attacker cannot induce collisions as easily as he might with a simple checksum.
Let's assume the input value is somewhat long
and has > 256 bits of entropy.
Then any approach like the current one, which truncates the sha256 result, will be less secure against collisions compared with with an approach that preserves all 256 bits.
Many practical use cases would require fewer than 256 bits of entropy. Suppose our analysis of traffic volume suggests the risk of collision would be acceptable when using just 128 bits. Is using the first half of the sha256 hash a secure approach? Yes, it is. As is the use of any similar subset, such as the last half. This is all subject to the obvious caveat that we chose to reduce the security parameter. In general, if you want N < 256 bits of entropy, reporting the N-bit prefix of a sha256() result is secure.
The most efficient way to send the bits is as raw binary via an 8-bit clean channel. If that's not available, for example because we need to put them in an URL, then we must do some sort of channel encoding as seen in the present source code. Notice that this will necessarily expand the message, which is the opposite of "compression" that you mentioned. To send a 24-bit message as hex we could send six ASCII characters, or as Base64 we could send four characters. If the message is initial prefix of sha256() output, then we have "truncated" the hash, trading bandwidth for security.
This code achieves its design goals. We might wish to slightly adjust the design. A small amount of refactoring is indicated.
I would be happy to delegate or accept maintenance tasks on this codebase.
You are using a JavaScript library to do this, which will result in terrible performance.
You are much better off using a native library. For instance, in NodeJS you can do the following:
const { createHash } = require('crypto');
function sha256inb64(value) {
const hash = createHash('sha256');
hash.update(value);
return hash.digest('base64');
}
I've deliberately kept the name short. This returns the digest directly as base 64 which is much faster to encode and doesn't limit the output size.
You can limit the number of output characters to 8 by using hash.digest('base64').substring(0, 8) in the code provided on top.
Note that this only provides (8 / 4) * 3 = 6 bytes or 48 bits of security, so it is not considered cryptographically secure anymore, rendering the SHA256 function next to useless.
If you don't need cryptographic security then there are a lot faster hashes available, see e.g. a comparison of hashes here.
function stringToInt(string) {
What does this even mean? What does it mean for a string to be put into an integer? Will it create an integer of any particular size? It doesn't constrain the input string for certain.
Otherwise the base conversion seems to be reasonable.
int = Math.floor(int / base);
This is definitely restricting the input size.
function intToString(int) {
Same problem with the naming, although it at least seems to be the reverse operation of stringToInt.
function sha_256(value) {
This name is a terrible name for this particular function. You're using a sha256 that - for some bad reason - always returns hexadecimals. Now by adding an underscore it suddenly returns the a value after "compression" in a different text encoding? It isn't explained why, why it is useful or that it is "compressed" as claimed.
let bits = hexToBytes(sha256(value) /* returns a hex string */ )
Wait, what? Does it return bits, bytes or hexadecimals? You are aware that those are different concepts, right? It returns bytes as indicated.
for (let i = 0; i < bits.length; i += 8) {
const a = bits[i];
const b = bits[i + 1];
const c = bits[i + 2];
const d = bits[i + 3];
const e = bits[i + 4];
const f = bits[i + 5];
const g = bits[i + 6];
const h = bits[i + 7];
const chunk = intToString(a + b + c + d + e + f + g + h);
str += chunk.padStart(2, '0');
}
Note I've used the correct but rolled back code of the question here, as the original code doesn't make any sense by itself. That edit should not have been rolled back.
If you're doing the counting instead of the computer then you're probably doing something wrong. You can add up to a total using an inner for loop.
Worse is that adding values is really dangerous when it comes to "compression"; it is of course clear that 1 + 2 will add up to the same value as 2 + 1. If you do that with a lot of values then it will allow an attacker to quickly find collisions. Just taking the first characters is likely more secure - and it cannot be less secure.