5
\$\begingroup\$

I think I've found a way to somewhat "compress" the length of a SHA-256 hash. I am doing some addition, so I wanted to know whether an approach like this is secure or not.

const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

function stringToInt(string) {
    const base = digits.length;
    let result = 0;
    for (let i = 0; i < string.length; i++) {
        result = result * base + digits.indexOf(string.charAt(i));
    }
    return result
}

function intToString(int) {
    const base = digits.length;
    let result = '';
    while (0 < int) {
        result = digits.charAt(int % base) + result;
        int = Math.floor(int / base);
    }
    return result || '0'
}
function sha_256(value) {
    function hexToBytes(hex) {
        let bytes = [];
        for (let c = 0; c < hex.length; c += 2)
            bytes.push(parseInt(hex.substr(c, 2), 16));
        return bytes;
    }
    let str = "";
    let bits = hexToBytes(sha256(value) /* returns a hex string */ )
    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];
        console.log(a, b, c, d, e, f, g, h)
    }
    return str
}
```
\$\endgroup\$
8
  • \$\begingroup\$ Oh crap, I just realized I sent the wrong code. \$\endgroup\$
    – rfl890
    Commented Apr 11, 2023 at 13:18
  • 1
    \$\begingroup\$ Post a new question, this question has been answered. Please do not edit the question, especially the code, after an answer has been posted. Changing the question may cause answer invalidation. Everyone needs to be able to see what the reviewer was referring to. What to do after the question has been answered. \$\endgroup\$
    – pacmaninbw
    Commented Apr 11, 2023 at 13:30
  • \$\begingroup\$ The code provided crashes and is not fit for review! \$\endgroup\$
    – Blindman67
    Commented Apr 11, 2023 at 18:50
  • \$\begingroup\$ @pacmaninbw, Blindman67 helpfully raises a recurring issue that I would be happy to pursue in some Meta thread. When interacting with colleagues or StackExchange collaborators I routinely take the author at his word, assuming he tested (some of) the code and finds it suitable to merge to main. I occasionally complain that author should have offered some combination of (1) example {in,out}put, (2) same thing in unit test form, (3) coverage measurement, (4) algorithm citation, (5) specification. The first two are amenable to automated process improvements in how new questions are accepted. \$\endgroup\$
    – J_H
    Commented Apr 11, 2023 at 23:18
  • \$\begingroup\$ @J_H When there is an executable program or unit tests I generally run the program to test it myself before I write a review. Beginners which is what we are mostly dealing with will not understand coverage measurement, most of them don't even do negative test paths. Yes specification or unit test is best. In this case what we can do is close the question, let the poster edit it as necessary and then go through the reopen process. That isn't really something I want to do with a hot network question. \$\endgroup\$
    – pacmaninbw
    Commented Apr 12, 2023 at 13:04

2 Answers 2

8
\$\begingroup\$

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.

\$\endgroup\$
1
  • \$\begingroup\$ Regarding while (0 < int), it's called a Yoda condition, and the general benefit is that it's resilient to some classes of errors. (i.e. if (foo = 0) would result in an assignment but if (0 = foo) would result in a compile-time error.) That said, many languages nowadays have linters that warn against such things, so IMHO it's not worth the steep impact to readability. \$\endgroup\$
    – Abion47
    Commented Apr 11, 2023 at 21:35
0
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.