Skip to content

Commit

Permalink
Updated the basic shuffleOutput to reduce odds of decoding hash of a …
Browse files Browse the repository at this point in the history
…different seed.
  • Loading branch information
licitdev committed Jul 7, 2019
1 parent 4d5aab9 commit 356d16f
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 46 deletions.
91 changes: 48 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
```

Expand Down Expand Up @@ -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'},
];
Expand All @@ -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`
Expand All @@ -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`
Expand All @@ -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`
Expand All @@ -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`
Expand All @@ -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`
Expand All @@ -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

Expand All @@ -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
22 changes: 20 additions & 2 deletions seeded-hashids.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down Expand Up @@ -87,7 +105,7 @@ function _encode(useHex, scope, data, seed) {
}

if(_shuffleOutput){
return _shuffleSeededString(hash, selectedHasher.alphabet);
return _doShuffleOutput(hash, seed);
}

return hash;
Expand All @@ -109,7 +127,7 @@ function _decode(useHex, scope, hash, seed) {
}

if(_shuffleOutput){
hash = _unshuffleSeededString(hash, selectedHasher.alphabet);
hash = _doUnshuffleOutput(hash, seed);
}

let data;
Expand Down
1 change: 0 additions & 1 deletion tests/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down

0 comments on commit 356d16f

Please sign in to comment.