Skip to content

Commit

Permalink
ft: ⭐ adding prng.pick() and prng.rand() functions
Browse files Browse the repository at this point in the history
Signed-off-by: Marco Aurélio da Silva <marcoonroad@gmail.com>
  • Loading branch information
marcoonroad committed Mar 2, 2020
1 parent 2e32b97 commit 39fd138
Show file tree
Hide file tree
Showing 9 changed files with 399 additions and 97 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -5,6 +5,9 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Vim files
*.swp

# Runtime data
pids
*.pid
Expand Down
114 changes: 103 additions & 11 deletions README.md
Expand Up @@ -40,9 +40,9 @@ To generate random sequences (paired on Brazillian lotteries if you want to run
any kind of audit person):

```javascript
const Lottery = spadille.lottery.brazillian
const megaSenaSequence = await Lottery.megaSena(secret, payload)
const federalNumbers = await Lottery.federal(secret, payload)
const Lottery = spadille.lottery.brazillian;
const megaSenaSequence = await Lottery.megaSena(secret, payload);
const federalNumbers = await Lottery.federal(secret, payload);
```

Here, `secret` is your "HMAC-signing"-key and `payload` is a user/session-derived content (possibly
Expand All @@ -61,20 +61,28 @@ const arbitrarySequence = await spadille.prng.generate({
maximum: maximumInclusiveValue,
amount: outputSequenceLength,
distinct: booleanFlag,
})
});
```

Such sequence can be made of many elements as you wish (but keep the eye on hardware limits, e.g,
the limits of 32-bits integer representation). The number of elements are configured by the `amount`
parameter. The `minimum` and `maximum` are point parameters for an inclusive interval (closed on
both sides). The `distinct` is a flag to compute the sequence of unique numbers (without repetitions).

If you want to generate a random number between 0 (closed interval) and 1 (open interval), there
is the wrapper function `spadille.prng.rand`, inspired on the classic Random API as found in
many programming languages in the wild. To use this function, just call:

```javascript
const randomFraction = await spadille.prng.rand(secret, payload);
```

Given that we can generate arbitrary sequences, the random permutation algorithm becomes
straightforward. This kind of permutation would just generate a random index sequence with
minimum as `0`, maximum as `inputSequence.length - 1` and amount as `inputSequence.length`,
where `inputSequence` is the list that we want to permute/shuffle. We then, in the end, use
such random index sequence to map `inputSequence` entries into an output sequence indexed
by such random index sequence. This wrapper function is implemented as an API below:
such random index sequence to map `inputSequence` entries into an output sequence by indexing
with the random index sequence. This wrapper function is implemented as an API below:

```javascript
const inputSequence = [ ... ] // an arbitrary list
Expand All @@ -89,6 +97,41 @@ const outputSequence = await spadille.prng.permute({
*/
```

Likewise, it's possible to take only a randomly ordered sub-sequence from the
original sequence. This wrapper function is called `pick` and the contract/typing
follows:

```javascript
const classes = ['warrior', 'rogue', 'mage', 'priest', 'hunter', 'alchemist'];
const partyClasses = await spadille.prng.pick({
secret,
payload,
sequence: classes,
distinct: true, // optional, default is `false` for pick()
amount: 3, // optional, default is `1` for pick()
});
```

Note that `{distinct: true}` only filters duplications on array-index-level, not
on array-value-level, it means that if your input sequence/array contains duplicated
values, they aren't deduplicated here. It also means if `{distinct: false}` and your
input sequence/array contain just unique values, it is possible to generate duplicated
values - it's all because picking is implemented on array-index-level generation.
The default behavior of `pick` is to retrieve just one random element from a given
sequence, but the output/result is still a list, thus, you will likely use the
following pattern in such cases:

```javascript
const [randomElement] = await spadille.prng.pick({secret, payload, sequence});
```

Note that `pick` will yield the same behavior of `permute` if you pass the same
`secret`, `payload`, sequence for both calls, and `{distinct: true}` with
`{amount: sequenceLength}` for `pick`. Therefore, `pick` is a generalisation/superset
of `permute`, and the latter can contain the underlying implementation calling the
former (actually this is not the case by now, but future refactor processes will end
on that code deduplication).

There's also a helper function provided to help you to generate fresh secrets.
By using cryptograpically secure PRNGs for both Node (through `crypto` OpenSSL
bindings) and browsers (through the `crypto` API), we ensure a good source of
Expand All @@ -97,19 +140,68 @@ can nevertheless convert to formats/encodings such as Base-64 and Hexadecimal.
Just pass the amount of bytes to generate and be happy with that! :)

```javascript
const amountOfBytes = 32
const noiseSecret = await spadille.secret.generate(amountOfBytes)
const amountOfBytes = 32;
const noiseSecret = await spadille.secret.generate(amountOfBytes);
```

Remember that once you generate such secret, you should store it somewhere
to retrieve later to "sign" the random sequences. And in the end, you should
also publish such secret in a commitment/opening style for public verification
by your users/clients.

Future plans include a `secret.generateBase64(amountOfBytes)` function to automatically
wrap the secret under Base64 format. By now, you will need to wrap using either
`btoa()` (browsers only) or `Buffer.from()` (Node.js only):

```javascript
// browsers only
const base64Secret = btoa(noiseSecret);
```

```javascript
// Node.js only
const base64Secret = Buffer.from(noiseSecret).toString('base64');
```

Future plans also include to automatically detect a Base64 string while generating and
verifying randomly generated values (to maintain backwards compatibility), nevertheless,
by now you'll also need to unwrap Base64 strings manually before passing them on this
library API:

```javascript
// browsers only
const secret = atob(base64Secret);
```

```javascript
// Node.js only
const secret = Buffer.from(base64Secret, 'base64').toString();
```

You can nevertheless create your own secrets by hand or through some other mechanism.
I don't recommend that if you want automated processes and stuff. This secret generation
uses the well-tested random bytes generation functions from browser vendors and Node.js.
If you stick on that manual approach by your own risk, ensure that:

- The secret contains high-quality entropy, using all available keyboard characters,
even special ones such as `*&@#!:~><_(%$)[]{^}?`, everything as printable
ASCII characters.
- The secret is a long enough string that make guessing impossible (I recommend above
48 characters if you use all available printable ones, even the special characters).
- The secret don't parses itself as Base64, that is, use the special characters above
to ensure that your secret is not a Base64 one, for instance, by appending them as
suffix when you generate custom secrets as random long numbers. Note that suffix
appending doesn't reduce the 48 characters security advise said above, you still
need to generate such amount of entropy and ensure its quality.

If your custom generated secret fit such cases above, you can pass them on this library
API without no worries of broken backwards compatibility in the future.

### Remarks

Any doubts, enter in touch.
Pull requests and issues are welcome! Have fun playing with this library! Happy hacking!

[1]: https://en.wikipedia.org/wiki/Provably_fair
[2]: https://cryptogambling.org/whitepapers/provably-fair-algorithms.pdf
[3]: https://unpkg.com/spadille/dist/index.js
[1]: https://en.wikipedia.org/wiki/Provably_fair
[2]: https://cryptogambling.org/whitepapers/provably-fair-algorithms.pdf
[3]: https://unpkg.com/spadille/dist/index.js
176 changes: 133 additions & 43 deletions __tests__/prng.js
Expand Up @@ -13,16 +13,20 @@ it('should generate arbitrary/huge luck numbers', async function () {

const payload = cuid()

const sequence = await support.call(function (secret, payload) {
return this.prng.generate({
secret,
payload,
minimum: 30,
maximum: 9000,
amount: 500,
distinct: false
})
}, SECRET, payload)
const sequence = await support.call(
function (secret, payload) {
return this.prng.generate({
secret,
payload,
minimum: 30,
maximum: 9000,
amount: 500,
distinct: false
})
},
SECRET,
payload
)

sequence.forEach(function (number) {
expect(number).toBeGreaterThanOrEqual(30)
Expand All @@ -38,21 +42,30 @@ it('should not enter in infinite loop while', async function () {

const payload = cuid()

const failed = await support.call(function (secret, payload) {
return this.prng.generate({
secret,
payload,
minimum: 7,
maximum: 10,
amount: 5,
distinct: true
}).then(function () {
return false
}).catch(function (reason) {
return reason.message ===
'The number of balls [amount] must not be greater than the [(maximum - minimum) + 1] number of RNG when [distinct] flag is on!'
})
}, SECRET, payload)
const failed = await support.call(
function (secret, payload) {
return this.prng
.generate({
secret,
payload,
minimum: 7,
maximum: 10,
amount: 5,
distinct: true
})
.then(function () {
return false
})
.catch(function (reason) {
return (
reason.message ===
'The number of balls [amount] must not be greater than the [(maximum - minimum) + 1] number of RNG when [distinct] flag is on!'
)
})
},
SECRET,
payload
)

expect(failed).toBe(true)
})
Expand All @@ -62,16 +75,20 @@ it('should pass if the interval is the same of amount when distinct enabled', as

const payload = cuid()

const sequence = await support.call(function (secret, payload) {
return this.prng.generate({
secret,
payload,
minimum: 11,
maximum: 60,
amount: 50,
distinct: true
})
}, SECRET, payload)
const sequence = await support.call(
function (secret, payload) {
return this.prng.generate({
secret,
payload,
minimum: 11,
maximum: 60,
amount: 50,
distinct: true
})
},
SECRET,
payload
)

sequence.forEach(function (number) {
expect(number).toBeGreaterThanOrEqual(11)
Expand All @@ -87,14 +104,19 @@ it('should shuffle a list', async function () {

const payload = cuid()

const inputSequence = [ 1, 2, 3, 4, 5, 6, 7 ]
const outputSequence = await support.call(function (secret, payload, inputSequence) {
return this.prng.permute({
secret,
payload,
inputSequence
})
}, SECRET, payload, inputSequence)
const inputSequence = [1, 2, 3, 4, 5, 6, 7]
const outputSequence = await support.call(
function (secret, payload, inputSequence) {
return this.prng.permute({
secret,
payload,
inputSequence
})
},
SECRET,
payload,
inputSequence
)

outputSequence.forEach(function (number) {
expect(number).toBeGreaterThanOrEqual(1)
Expand All @@ -104,3 +126,71 @@ it('should shuffle a list', async function () {

expect(outputSequence.length).toBe(7)
})

it('should generate a random rational number [>= 0] and [< 1]', async function () {
expect.assertions(2)

const payload = cuid()

const fraction = await support.call(
function (secret, payload) {
return this.prng.rand(secret, payload)
},
SECRET,
payload
)

expect(fraction).toBeGreaterThanOrEqual(0)
expect(fraction).toBeLessThan(1)
})

it('should pick a random element from given set', async function () {
expect.assertions(1)

const secret = '<a military secret>'
const payload = '{requestId:16}'
const fruits = ['apple', 'banana', 'orange']
const [fruit] = await support.call(
function (secret, payload, sequence) {
return this.prng.pick({ secret, payload, sequence })
},
secret,
payload,
fruits
)

expect(fruit).toBe('apple')
})

it('should make pick=permute when distinct=true & amount=len(seq)', async function () {
expect.assertions(1)

const payload = cuid()
const sequence = ['A', 'B', 'C', 'D', 'E', 'F', 'G']

const permuteOutput = await support.call(
function (secret, payload, inputSequence) {
return this.prng.permute({ secret, payload, inputSequence })
},
SECRET,
payload,
sequence
)

const pickOutput = await support.call(
function (secret, payload, sequence) {
return this.prng.pick({
secret,
payload,
sequence,
amount: sequence.length,
distinct: true
})
},
SECRET,
payload,
sequence
)

expect(pickOutput).toEqual(permuteOutput)
})
4 changes: 2 additions & 2 deletions build/spadille/prng/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 39fd138

Please sign in to comment.