Generate a Hash from string in Javascript

前端 未结 22 899
不知归路
不知归路 2020-11-22 03:34

I need to convert strings to some form of hash. Is this possible in JavaScript?

I\'m not utilizing a server-side language so I can\'t do it that way.

相关标签:
22条回答
  • 2020-11-22 04:18

    Thanks to the example by mar10, I found a way to get the same results in C# AND Javascript for an FNV-1a. If unicode chars are present, the upper portion is discarded for the sake of performance. Don't know why it would be helpful to maintain those when hashing, as am only hashing url paths for now.

    C# Version

    private static readonly UInt32 FNV_OFFSET_32 = 0x811c9dc5;   // 2166136261
    private static readonly UInt32 FNV_PRIME_32 = 0x1000193;     // 16777619
    
    // Unsigned 32bit integer FNV-1a
    public static UInt32 HashFnv32u(this string s)
    {
        // byte[] arr = Encoding.UTF8.GetBytes(s);      // 8 bit expanded unicode array
        char[] arr = s.ToCharArray();                   // 16 bit unicode is native .net 
    
        UInt32 hash = FNV_OFFSET_32;
        for (var i = 0; i < s.Length; i++)
        {
            // Strips unicode bits, only the lower 8 bits of the values are used
            hash = hash ^ unchecked((byte)(arr[i] & 0xFF));
            hash = hash * FNV_PRIME_32;
        }
        return hash;
    }
    
    // Signed hash for storing in SQL Server
    public static Int32 HashFnv32s(this string s)
    {
        return unchecked((int)s.HashFnv32u());
    }
    

    JavaScript Version

    var utils = utils || {};
    
    utils.FNV_OFFSET_32 = 0x811c9dc5;
    
    utils.hashFnv32a = function (input) {
        var hval = utils.FNV_OFFSET_32;
    
        // Strips unicode bits, only the lower 8 bits of the values are used
        for (var i = 0; i < input.length; i++) {
            hval = hval ^ (input.charCodeAt(i) & 0xFF);
            hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
        }
    
        return hval >>> 0;
    }
    
    utils.toHex = function (val) {
        return ("0000000" + (val >>> 0).toString(16)).substr(-8);
    }
    
    0 讨论(0)
  • 2020-11-22 04:19

    About half of the answers here are the same String.hashCode hash function taken from Java. It dates back to 1981 from Gosling Emacs, is extremely weak, and makes zero sense performance-wise in modern JavaScript. In fact, implementations could be significantly faster by using ES6 Math.imul, but no one took notice. We can do much better than this, at essentially identical performance.

    Here's something I did—cyrb53, a simple but high quality 53-bit hash. It's quite fast, provides very good hash distribution, and has significantly lower collision rates compared to any 32-bit hash.

    const cyrb53 = function(str, seed = 0) {
        let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
        for (let i = 0, ch; i < str.length; i++) {
            ch = str.charCodeAt(i);
            h1 = Math.imul(h1 ^ ch, 2654435761);
            h2 = Math.imul(h2 ^ ch, 1597334677);
        }
        h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
        h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
        return 4294967296 * (2097151 & h2) + (h1>>>0);
    };
    

    It is similar to the well-known MurmurHash/xxHash algorithms, it uses a combination of multiplication and Xorshift to generate the hash, but not as thorough. As a result it's faster than either in JavaScript and significantly simpler to implement.

    Like any proper hash, it has an avalanche effect, which basically means small changes in the input have big changes in the output making the resulting hash appear more 'random':

    "501c2ba782c97901" = cyrb53("a")
    "459eda5bc254d2bf" = cyrb53("b")
    "fbce64cc3b748385" = cyrb53("revenge")
    "fb1d85148d13f93a" = cyrb53("revenue")
    

    You can also supply a seed for alternate streams of the same input:

    "76fee5e6598ccd5c" = cyrb53("revenue", 1)
    "1f672e2831253862" = cyrb53("revenue", 2)
    "2b10de31708e6ab7" = cyrb53("revenue", 3)
    

    Technically, it is a 64-bit hash, that is, two uncorrelated 32-bit hashes computed in parallel, but JavaScript is limited to 53-bit integers. If convenient, the full 64-bit output can be used by altering the return statement with a hex string or array.

    return [h2>>>0, h1>>>0];
    // or
    return (h2>>>0).toString(16).padStart(8,0)+(h1>>>0).toString(16).padStart(8,0);
    

    Be aware that constructing hex strings drastically slows down batch processing. The array is more efficient, but obviously requires two checks instead of one.


    Just for fun, here's the smallest hash I could come up with that's still decent. It's a 32-bit hash in 89 chars with better quality randomness than even FNV or DJB2:

    TSH=s=>{for(var i=0,h=9;i<s.length;)h=Math.imul(h^s.charCodeAt(i++),9**9);return h^h>>>9}
    
    0 讨论(0)
  • 2020-11-22 04:19

    I needed a similar function (but different) to generate a unique-ish ID based on the username and current time. So:

    window.newId = ->
      # create a number based on the username
      unless window.userNumber?
        window.userNumber = 0
      for c,i in window.MyNamespace.userName
        char = window.MyNamespace.userName.charCodeAt(i)
        window.MyNamespace.userNumber+=char
      ((window.MyNamespace.userNumber + Math.floor(Math.random() * 1e15) + new Date().getMilliseconds()).toString(36)).toUpperCase()
    

    Produces:

    2DVFXJGEKL
    6IZPAKFQFL
    ORGOENVMG
    ... etc 
    

    edit Jun 2015: For new code I use shortid: https://www.npmjs.com/package/shortid

    0 讨论(0)
  • 2020-11-22 04:19

    I'm kinda late to the party, but you can use this module: crypto:

    const crypto = require('crypto');
    
    const SALT = '$ome$alt';
    
    function generateHash(pass) {
      return crypto.createHmac('sha256', SALT)
        .update(pass)
        .digest('hex');
    }
    

    The result of this function is always is 64 characters string; something like this: "aa54e7563b1964037849528e7ba068eb7767b1fab74a8d80fe300828b996714a"

    0 讨论(0)
  • 2020-11-22 04:23

    Note: Even with the best 32-bit hash, collisions will occur sooner or later.

    The hash collision probablility can be calculated as 1 - e ^ (-k(k-1) / 2N, aproximated as k^2 / 2N (see here). This may be higher than intuition suggests:
    Assuming a 32-bit hash and k=10,000 items, a collision will occur with a probablility of 1.2%. For 77,163 samples the probability becomes 50%! (calculator).
    I suggest a workaround at the bottom.

    In an answer to this question Which hashing algorithm is best for uniqueness and speed?, Ian Boyd posted a good in depth analysis. In short (as I interpret it), he comes to the conclusion that Murmur is best, followed by FNV-1a.
    Java’s String.hashCode() algorithm that esmiralha proposed seems to be a variant of DJB2.

    • FNV-1a has a a better distribution than DJB2, but is slower
    • DJB2 is faster than FNV-1a, but tends to yield more collisions
    • MurmurHash3 is better and faster than DJB2 and FNV-1a (but the optimized implementation requires more lines of code than FNV and DJB2)

    Some benchmarks with large input strings here: http://jsperf.com/32-bit-hash
    When short input strings are hashed, murmur's performance drops, relative to DJ2B and FNV-1a: http://jsperf.com/32-bit-hash/3

    So in general I would recommend murmur3.
    See here for a JavaScript implementation: https://github.com/garycourt/murmurhash-js

    If input strings are short and performance is more important than distribution quality, use DJB2 (as proposed by the accepted answer by esmiralha).

    If quality and small code size are more important than speed, I use this implementation of FNV-1a (based on this code).

    /**
     * Calculate a 32 bit FNV-1a hash
     * Found here: https://gist.github.com/vaiorabbit/5657561
     * Ref.: http://isthe.com/chongo/tech/comp/fnv/
     *
     * @param {string} str the input value
     * @param {boolean} [asString=false] set to true to return the hash value as 
     *     8-digit hex string instead of an integer
     * @param {integer} [seed] optionally pass the hash of the previous chunk
     * @returns {integer | string}
     */
    function hashFnv32a(str, asString, seed) {
        /*jshint bitwise:false */
        var i, l,
            hval = (seed === undefined) ? 0x811c9dc5 : seed;
    
        for (i = 0, l = str.length; i < l; i++) {
            hval ^= str.charCodeAt(i);
            hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
        }
        if( asString ){
            // Convert to 8 digit hex string
            return ("0000000" + (hval >>> 0).toString(16)).substr(-8);
        }
        return hval >>> 0;
    }
    

    Improve Collision Probability

    As explained here, we can extend the hash bit size using this trick:

    function hash64(str) {
        var h1 = hash32(str);  // returns 32 bit (as 8 byte hex string)
        return h1 + hash32(h1 + str);  // 64 bit (as 16 byte hex string)
    }
    

    Use it with care and don't expect too much though.

    0 讨论(0)
  • 2020-11-22 04:23

    I went for a simple concatenation of char codes converted to hex strings. This serves a relatively narrow purpose, namely just needing a hash representation of a SHORT string (e.g. titles, tags) to be exchanged with a server side that for not relevant reasons can't easily implement the accepted hashCode Java port. Obviously no security application here.

    String.prototype.hash = function() {
      var self = this, range = Array(this.length);
      for(var i = 0; i < this.length; i++) {
        range[i] = i;
      }
      return Array.prototype.map.call(range, function(i) {
        return self.charCodeAt(i).toString(16);
      }).join('');
    }
    

    This can be made more terse and browser-tolerant with Underscore. Example:

    "Lorem Ipsum".hash()
    "4c6f72656d20497073756d"
    

    I suppose if you wanted to hash larger strings in similar fashion you could just reduce the char codes and hexify the resulting sum rather than concatenate the individual characters together:

    String.prototype.hashLarge = function() {
      var self = this, range = Array(this.length);
      for(var i = 0; i < this.length; i++) {
        range[i] = i;
      }
      return Array.prototype.reduce.call(range, function(sum, i) {
        return sum + self.charCodeAt(i);
      }, 0).toString(16);
    }
    
    'One time, I hired a monkey to take notes for me in class. I would just sit back with my mind completely blank while the monkey scribbled on little pieces of paper. At the end of the week, the teacher said, "Class, I want you to write a paper using your notes." So I wrote a paper that said, "Hello! My name is Bingo! I like to climb on things! Can I have a banana? Eek, eek!" I got an F. When I told my mom about it, she said, "I told you, never trust a monkey!"'.hashLarge()
    "9ce7"
    

    Naturally more risk of collision with this method, though you could fiddle with the arithmetic in the reduce however you wanted to diversify and lengthen the hash.

    0 讨论(0)
提交回复
热议问题