From 356d16ff92418d51cee5fcaf8da33af7846ebab7 Mon Sep 17 00:00:00 2001 From: ian Date: Sun, 7 Jul 2019 06:48:45 +0000 Subject: [PATCH] Updated the basic shuffleOutput to reduce odds of decoding hash of a different seed. --- README.md | 91 +++++++++++++++++++++++++---------------------- seeded-hashids.js | 22 ++++++++++-- tests/test.js | 1 - 3 files changed, 68 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index f9c8df2..d927c71 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,14 @@ Generate seeded Hashids that is unique per scope. [![NPM version][npm-version-image]][npm-url] [![License][license-image]][license-url] -**Seeded-Hashids** is an easy to use library to generate seeded [Hashids](http://hashids.org/javascript) which is unique to a seed based on a user or group. Hide the raw userids, hex strings or objectids from end users. +**Seeded-Hashids** is an easy to use library to generate seeded [Hashids](http://hashids.org/javascript) which is unique to a seed based on a user or group. Hide the raw ids, hex strings, objectids or uuids from end users. An example is to generate Hashids that are unique to a particular application. Even if multiple applications shared their userids with each other, the users could not be correlated or identified by their userids. +## Sample Scenario ``` -Sample scenario... -Encoding the userids and using their actual userid as a seed which is unique and never revealed. +Encoding the userids and using their actual userid as a seed. +The userids are unique and never revealed to end users. User A (ID 123) User B (ID 456) User C (ID 789) @@ -23,7 +24,7 @@ Application B sees User A as 'qweasd' and sees User C as 'rtyjkl' Application C sees User A as 'fghzxc' and sees User B as 'asdiop' 'asdiop' is supposedly only visible to Application C. -If Application A decodes 'asdiop', it will be (ID 654) which is not the same user. +If Application A decodes 'asdiop', it decodes to an empty string. ``` ## Getting started @@ -36,42 +37,42 @@ Install Seeded-Hashids via: Sample code: ```javascript -var seededHashids = require('seeded-hashids'); -var ObjectId = require('mongoose').Types.ObjectId; -var scopes = [ +const seededHashids = require('seeded-hashids'); +const ObjectId = require('mongoose').Types.ObjectId; +const scopes = [ {scope: 'user', salt: 'some-salt'} ]; seededHashids.initialize({scopes: scopes, objectId: ObjectId}); -var encoded, decoded; +let encoded, decoded; // Encoding hex strings encoded = seededHashids.encodeHex('user', 'abcd1234'); decoded = seededHashids.decodeHex('user', encoded); -console.log(encoded); // 'dAMW5Em6' +console.log(encoded); // 'rdksH67E' console.log(decoded); // 'abcd1234' // Encoding hex strings with seed encoded = seededHashids.encodeHex('user', 'abcd1234', 'unique-seed'); decoded = seededHashids.decodeHex('user', encoded, 'unique-seed'); -console.log(encoded); // 'MVVEdMKq' +console.log(encoded); // 'dvdztVza' console.log(decoded); // 'abcd1234' -// If a wrong seed is used to decode, a different output will be +// If a wrong seed is used to decode, will decode to a different output decoded = seededHashids.decodeHex('user', encoded, 'wrong-seed'); -console.log(decoded); // 'cabd2341' (Different) +console.log(decoded); // '' (Empty string) // Decoding ObjectIds, same as hex but needs to be 24 characters hex string encoded = seededHashids.encodeHex('user', 'abcd1234abcd1234abcd1234', 'unique-seed'); decoded = seededHashids.decodeObjectId('user', encoded, 'unique-seed'); -console.log(encoded); // 'g9jM7B94VjJQhWj4AVNVqE' +console.log(encoded); // 'U5FdAz8EvEErzga96Z5z6S' console.log(decoded); // ObjectId('abcd1234abcd1234abcd1234') // Encoding positive integers encoded = seededHashids.encode('user', 12345678); decoded = seededHashids.decode('user', encoded); -console.log(encoded); // 'ezBbrM' +console.log(encoded); // 'vxfR8swj' console.log(decoded); // 12345678 ``` @@ -103,7 +104,7 @@ objectId | no | `ObjectId` | - - The array is a list of scope object that contains a scope string and a salt string. - Each scope could be then name of a class or an object type. ```javascript -var scope = [ +let scope = [ {scope: 'user', salt: 'some-salt'}, {scope: 'profile', salt: 'another-salt'}, ]; @@ -113,33 +114,33 @@ var scope = [ - This value is passed directly to Hashids. - A minimum of 16 unique characters are required. ```javascript -var charset = 'abcdefghijkmnopqrstuvwxyz0123456789'; +let charset = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'; ``` ##### hashLength `Number` *(optional)* - This value is passed directly to Hashids, which adds padding to reach the length required. ```javascript -var hashLength = 8; +let hashLength = 8; ``` ##### shuffleOutput `Boolean` *(optional)* -- This value determines if the hash will be shuffled after encoding by Hashids and before decoding by Hashids. -- Each scope shuffles the output differently. +- This value determines if the output hash will be shuffled after encoding by Hashids and before decoding by Hashids. +- The output is shuffled based on the seed and attempts to prevent decoding using a wrong seed. ```javascript -var shuffleOutput = true; +let shuffleOutput = true; ``` ##### objectId `Function` *(optional)* - This object is required only if there is a need to cast the decoding output to an ObjectId using .decodeObjectId. - Can pass in `require('mongoose').Types.ObjectId ` or `require('mongodb').ObjectId` or functions. ```javascript -var objectId = require('mongoose').Types.ObjectId; +let objectId = require('mongoose').Types.ObjectId; ``` --- ### **encode (scope, number, [seed])** : Hashid `String` > To encode positive numbers. ```javascript -var userId = seededHashids.encode('user', 12345678); +let userId = seededHashids.encode('user', 12345678); ``` #### scope `String` @@ -154,7 +155,7 @@ var userId = seededHashids.encode('user', 12345678); ### **encodeHex (scope, hex, [seed])** : Hashid `String` > To encode hex strings. ```javascript -var userId = seededHashids.encodeHex('user', 'abcd1234', 'unique-seed'); +let userId = seededHashids.encodeHex('user', 'abcd1234', 'unique-seed'); ``` #### scope `String` @@ -168,9 +169,9 @@ var userId = seededHashids.encodeHex('user', 'abcd1234', 'unique-seed'); --- ### **decode (scope, hash, [seed])** : decodedNumber `Number` -> To decode hashes into positive numbers. +> To decode hashes into positive numbers. Returns NaN if unable to decode. ```javascript -var userId = seededHashids.decode('user', 'X3e8L9EG', 'unique-seed'); +let userId = seededHashids.decode('user', 'vxfR8swj', 'unique-seed'); ``` #### scope `String` @@ -183,9 +184,9 @@ var userId = seededHashids.decode('user', 'X3e8L9EG', 'unique-seed'); - This seed is used to decode a hashid that is "unique" for itself. --- ### **decodeHex (scope, hash, [seed])** : decodedHex `String` -> To decode hashes into hex strings. +> To decode hashes into hex strings. Returns an empty string if unable to decode. ```javascript -var userId = seededHashids.decodeHex('user', 'MVVEdMKq', 'unique-seed'); +let userId = seededHashids.decodeHex('user', 'dvdztVza', 'unique-seed'); ``` #### scope `String` @@ -198,9 +199,9 @@ var userId = seededHashids.decodeHex('user', 'MVVEdMKq', 'unique-seed'); - This seed is used to decode a hashid that is "unique" for itself. --- ### **decodeObjectId (scope, hash, [seed])** : decodedObjectId `ObjectId` -> To decode hashes into objectIds. +> To decode hashes into objectIds. Returns NaN if unable to decode. ```javascript -var userId = seededHashids.decodeObjectId('user', 'g9jM7B94VjJQhWj4AVNVqE', 'unique-seed'); +let userId = seededHashids.decodeObjectId('user', 'U5FdAz8EvEErzga96Z5z6S', 'unique-seed'); ``` #### scope `String` @@ -221,50 +222,54 @@ seededHashids.reset(); ### **isInitialized ()** : isInitialized `Boolean` > To check if seededHashids is initialized. ```javascript -var isInitialized = seededHashids.isInitialized(); +let isInitialized = seededHashids.isInitialized(); ``` --- ### **getScopes ()** : scopes `Array` > To get the string array of scopes. ```javascript -var scopes = seededHashids.getScopes(); +let scopes = seededHashids.getScopes(); ``` --- ### **getCharset ()** : charset `String` > To get the charset string. ```javascript -var charset = seededHashids.getCharset(); +let charset = seededHashids.getCharset(); ``` --- ### **getHashLength ()** : hashLength `Number` > To get the hash length. ```javascript -var hashLength = seededHashids.getHashLength(); +let hashLength = seededHashids.getHashLength(); ``` --- ### **getShuffleOutput ()** : shuffleOutput `Boolean` > To check if the output is shuffled. ```javascript -var shuffleOutput = seededHashids.getShuffleOutput(); +let shuffleOutput = seededHashids.getShuffleOutput(); ``` --- ### **getObjectId ()** : objectId `Function` > To get the objectId function to see if available. ```javascript -var objectId = seededHashids.getObjectId(); +let objectId = seededHashids.getObjectId(); ``` +## Recommendations +1. Charset should **not** be too short. +2. Salts should **not** be too short. +3. Seeds should **not** be too short. Recommended to use **long** hex strings such as ObjectIds or UUIDs. +4. Encode **longer** hex strings such as ObjectIds or UUIDs. +5. Always **validate** the output after decoding. +6. Leave the shuffleOutput as **true**, which is the default value. +7. Encode and decode as required, recommended for database to contain only **original** ids or hex strings. ## Pitfalls 1. Encoding of an array of numbers is **not** supported. 2. Encoding of negative numbers are **not** supported. -3. The uniqueness of output **if seeded** is highly dependent on what the encoded data is and the seed. -4. Do not use this library as a security tool and do not encode sensitive data. This is **not** an encryption library. - -## Recommendations -1. Salts should not be too short. -2. Seeds should not be too short. Recommended to use **long** hex strings such as ObjectIds. -2. Recommend to encode **longer** hex strings such as ObjectIds with a long salt to increase "uniqueness". +3. Required to pass in the **correct type** of parameters in order to prevent invalid hashes by accident. +4. It is still **possible** for a different seed to decode a hash, but it is really rare if the **recommendations** are followed. +4. Do **not** use this library as a security tool and do not encode sensitive data. This is **not** an encryption library. ## License @@ -281,4 +286,4 @@ MIT License. See the [LICENSE](LICENSE) file. [npm-url]: https://www.npmjs.com/package/seeded-hashids [license-url]: https://github.com/licitdev/seeded-hashids/blob/master/LICENSE -[license-image]: https://img.shields.io/packagist/l/hashids/hashids.svg?style=flat +[license-image]: https://img.shields.io/packagist/l/hashids/hashids.svg?style=flat \ No newline at end of file diff --git a/seeded-hashids.js b/seeded-hashids.js index 6c68b79..6f30692 100644 --- a/seeded-hashids.js +++ b/seeded-hashids.js @@ -42,6 +42,24 @@ function _getShuffledCharset(seed) { return _shuffleSeededString(_charset, seed); } +function _doShuffleOutput(hash, seed) { // Can accept undefined if no seed + let shuffledCharset = _getShuffledCharset(seed); + let outputHash = ''; + for(let x = 0; x < hash.length; x++){ + outputHash += shuffledCharset[_charset.indexOf(hash[x])] + } + return outputHash; +} + +function _doUnshuffleOutput(hash, seed) { // Can accept undefined if no seed + let shuffledCharset = _getShuffledCharset(seed); + let outputHash = ''; + for(let x = 0; x < hash.length; x++){ + outputHash += _charset[shuffledCharset.indexOf(hash[x])] + } + return outputHash; +} + function _encode(useHex, scope, data, seed) { let selectedHasher = _scopes[scope]; @@ -87,7 +105,7 @@ function _encode(useHex, scope, data, seed) { } if(_shuffleOutput){ - return _shuffleSeededString(hash, selectedHasher.alphabet); + return _doShuffleOutput(hash, seed); } return hash; @@ -109,7 +127,7 @@ function _decode(useHex, scope, hash, seed) { } if(_shuffleOutput){ - hash = _unshuffleSeededString(hash, selectedHasher.alphabet); + hash = _doUnshuffleOutput(hash, seed); } let data; diff --git a/tests/test.js b/tests/test.js index 9ad059d..ba16015 100644 --- a/tests/test.js +++ b/tests/test.js @@ -513,7 +513,6 @@ describe('when encoding and decoding with shuffle', () => { let wrongSeed = 'wrongseed'; let encoded = seededHashids.encodeHex('user', hex, seed); let decoded = seededHashids.decodeObjectId('user', encoded, wrongSeed); - decoded = decoded.toString() assert.notDeepEqual(hex, decoded); });