├── docs ├── CNAME ├── Gemfile ├── _config.yml ├── Gemfile.lock └── index.md ├── .babelrc ├── .travis.yml ├── src └── spadille │ ├── lottery │ ├── index.js │ └── brazillian.js │ ├── secret │ ├── node.js │ ├── browser.js │ └── index.js │ ├── index.js │ ├── hmac │ ├── node.js │ ├── index.js │ └── browser.js │ ├── base64 │ ├── browser.js │ ├── node.js │ └── index.js │ ├── utils │ ├── bytes.js │ ├── environment.js │ └── index.js │ └── prng │ └── index.js ├── .github └── workflows │ └── nodejs.yml ├── azure-pipelines.yml ├── rollup.config.js ├── LICENSE ├── types └── spadille.d.ts ├── .gitignore ├── __tests__ ├── bias.js ├── secret.js ├── lottery.js └── prng.js ├── support └── index.js ├── package.json ├── dist └── index.js └── README.md /docs/CNAME: -------------------------------------------------------------------------------- 1 | spadille.marcoonroad.dev -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'github-pages', '>= 227', group: :jekyll_plugins 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "targets": { "node": "6" } } ] 4 | ], 5 | "plugins": ["transform-es2015-modules-commonjs"] 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8" 5 | - "10" 6 | - node 7 | 8 | script: 9 | - yarn run report 10 | 11 | cache: 12 | yarn: true 13 | -------------------------------------------------------------------------------- /src/spadille/lottery/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env node, browser */ 4 | /* eslint 5 | semi: off */ 6 | 7 | const brazillian = require('./brazillian'); 8 | 9 | module.exports.brazillian = brazillian; 10 | -------------------------------------------------------------------------------- /src/spadille/secret/node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint 4 | semi: off */ 5 | /* eslint-env node */ 6 | 7 | module.exports.init = function () { 8 | const crypto = require('crypto'); 9 | 10 | const secret = async function (bytes) { 11 | const buffer = crypto.randomBytes(bytes); 12 | return buffer.toString('ascii'); 13 | }; 14 | 15 | return { secret }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/spadille/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env browser, env */ 4 | /* eslint 5 | semi: off */ 6 | 7 | const lottery = require('./lottery'); 8 | const prng = require('./prng'); 9 | const secret = require('./secret'); 10 | const base64 = require('./base64'); 11 | 12 | module.exports.lottery = lottery; 13 | module.exports.prng = prng; 14 | module.exports.secret = secret; 15 | module.exports.base64 = base64; 16 | -------------------------------------------------------------------------------- /src/spadille/hmac/node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env node */ 4 | /* eslint 5 | semi: off */ 6 | 7 | module.exports.init = function () { 8 | const crypto = require('crypto'); 9 | const HASH = 'sha512'; 10 | 11 | const hmac = async function (secret, message) { 12 | const hash = crypto.createHmac(HASH, secret); 13 | 14 | hash.update(message); 15 | return hash.digest('hex'); 16 | }; 17 | 18 | return { hmac }; 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: yarn 21 | - run: yarn test 22 | env: 23 | CI: true 24 | -------------------------------------------------------------------------------- /src/spadille/secret/browser.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | 'use strict'; 4 | 5 | /* eslint 6 | no-new-func: off, 7 | semi: off */ 8 | /* eslint-env browser */ 9 | 10 | module.exports.init = function () { 11 | const Bytes = require('../utils/bytes'); 12 | const crypto = window.crypto; 13 | 14 | const secret = async function (bytes) { 15 | const buffer = crypto.getRandomValues(new Uint8Array(bytes)); 16 | return Bytes.toString(buffer, 'ascii'); 17 | }; 18 | 19 | return { secret }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/spadille/base64/browser.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | 'use strict'; 4 | 5 | /* eslint 6 | no-new-func: off, 7 | semi: off */ 8 | /* eslint-env browser */ 9 | 10 | module.exports.init = function () { 11 | return { 12 | toBase64: new Function(`return function (binary) { 13 | return btoa (unescape (encodeURIComponent (binary))); 14 | }`)(), 15 | fromBase64: new Function(`return function (base64) { 16 | return decodeURIComponent (escape (atob (base64))); 17 | }`)() 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: spadille 2 | description: "Verifiable/deterministic fair tickets generation for lotteries, raffles and gambling games." 3 | theme: jekyll-theme-dinky 4 | repository: marcoonroad/spadille 5 | show_downloads: false 6 | google_analytics: UA-41209773-2 7 | exclude: 8 | - Gemfile 9 | - Gemfile.lock 10 | - CNAME 11 | - vendor 12 | plugins: 13 | #- jekyll-feed 14 | #- jekyll-redirect-from 15 | #- jekyll-seo-tag 16 | #- jekyll-sitemap 17 | - jekyll-avatar 18 | - jemoji 19 | - jekyll-mentions 20 | #- jekyll-include-cache -------------------------------------------------------------------------------- /src/spadille/secret/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env browser, node */ 4 | /* eslint 5 | semi: off */ 6 | 7 | const { 8 | isNode, isBrowser 9 | } = require('../utils/environment'); 10 | 11 | let secret = null; 12 | 13 | (function () { 14 | /* istanbul ignore next */ 15 | if (isBrowser()) { 16 | secret = require('./browser').init().secret; 17 | } else if (isNode()) { 18 | secret = require('./node').init().secret; 19 | } else { 20 | throw Error('Could not detect execution context!'); 21 | } 22 | })(); 23 | 24 | const generate = secret; 25 | 26 | module.exports.generate = generate; 27 | -------------------------------------------------------------------------------- /src/spadille/base64/node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint 4 | no-new-func: off, 5 | semi: off */ 6 | /* eslint-env node */ 7 | 8 | module.exports.init = function () { 9 | // NOTE: eval is a workaround for Rollup bogus behavior injecting 10 | // variable browser-undefined module into bundle build context 11 | const toBase64 = new Function(`return function (binary) { 12 | return Buffer.from(binary, 'utf-8').toString('base64'); 13 | };`)(); 14 | const fromBase64 = new Function(`return function (payload) { 15 | return Buffer.from(payload, 'base64').toString('utf-8'); 16 | };`)(); 17 | 18 | return { 19 | toBase64, 20 | fromBase64 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/spadille/utils/bytes.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | 'use strict' 4 | 5 | /* eslint semi: off */ 6 | 7 | const fromString = function (text) { 8 | const encoder = new TextEncoder() 9 | const buffer = encoder.encode(text) 10 | const list = JSON.parse(`[${buffer.toString()}]`) 11 | const array = new Uint8Array(list.length) 12 | list.forEach(function (value, index) { 13 | array[index] = value 14 | }) 15 | return array 16 | } 17 | 18 | const toString = function (bytes, encoding) { 19 | encoding = encoding || 'utf-8' 20 | const decoder = new TextDecoder(encoding) 21 | return decoder.decode(bytes) 22 | } 23 | 24 | module.exports.fromString = fromString 25 | module.exports.toString = toString 26 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'ubuntu-latest' 11 | 12 | #steps: 13 | #- task: NodeTool@0 14 | # inputs: 15 | # versionSpec: '10.x' 16 | # displayName: 'Install Node.js' 17 | 18 | #pool: 19 | # vmImage: 'ubuntu-16.04' 20 | strategy: 21 | matrix: 22 | node_8_x: 23 | node_version: 8.x 24 | node_10_x: 25 | node_version: 10.x 26 | 27 | steps: 28 | - task: NodeTool@0 29 | inputs: 30 | versionSpec: $(node_version) 31 | 32 | - script: | 33 | yarn 34 | yarn run test 35 | displayName: 'yarn install and test' 36 | -------------------------------------------------------------------------------- /src/spadille/hmac/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env node */ 4 | /* eslint 5 | semi: off */ 6 | 7 | const { 8 | isNode, isBrowser 9 | } = require('../utils/environment'); 10 | 11 | let hmac = null; 12 | 13 | (function () { 14 | /* istanbul ignore next */ 15 | if (isBrowser()) { 16 | hmac = require('./browser').init().hmac; 17 | } else if (isNode()) { 18 | hmac = require('./node').init().hmac; 19 | } else { 20 | throw Error('Could not detect execution context!'); 21 | } 22 | })(); 23 | 24 | const sign = hmac; 25 | 26 | /* 27 | const verify = async function (secret, signature, message) { 28 | const result = await hmac(secret, message); 29 | 30 | return signature === result; 31 | }; 32 | */ 33 | 34 | module.exports.sign = sign; 35 | // module.exports.verify = verify; 36 | -------------------------------------------------------------------------------- /src/spadille/lottery/brazillian.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env node, browser */ 4 | /* eslint 5 | semi: off */ 6 | 7 | const utils = require('../utils'); 8 | const prng = require('../prng'); 9 | 10 | // unique six numbers from 1 up to 60, default case 11 | const megaSena = async function (secret, payload) { 12 | const numbers = await prng.generate({ secret, payload }); 13 | return utils.sortArrayNumber(numbers); 14 | }; 15 | 16 | // numbers between 00000 and 99999 17 | const federal = async function (secret, payload) { 18 | const sequence = await prng.generate({ 19 | secret, 20 | payload, 21 | minimum: 0, 22 | maximum: 9, 23 | amount: 5, 24 | distinct: false 25 | }); 26 | 27 | return sequence.map(function (number) { 28 | return number.toString(); 29 | }).join(''); 30 | }; 31 | 32 | module.exports.megaSena = megaSena; 33 | module.exports.federal = federal; 34 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const commonjs = require('rollup-plugin-commonjs') 4 | const nodeResolve = require('rollup-plugin-node-resolve') 5 | const terser = require('rollup-plugin-terser').terser 6 | const builtins = require('rollup-plugin-node-builtins') 7 | const globals = require('rollup-plugin-node-globals') 8 | 9 | module.exports = { 10 | input: 'src/spadille/index.js', 11 | output: { 12 | file: 'dist/index.js', 13 | format: 'iife', 14 | name: 'spadille', 15 | compact: true, 16 | exports: 'named', 17 | globals: { 18 | 'crypto': 'crypto' 19 | } 20 | }, 21 | external: ['crypto'], 22 | plugins: [ 23 | builtins({ crypto: true }), 24 | globals(), 25 | nodeResolve({ 26 | // module: true, 27 | // main: true, 28 | mainFields: ['module', 'main'], 29 | browser: true 30 | }), 31 | commonjs({ 32 | exclude: ['node_modules/**'], 33 | sourceMap: false 34 | }), 35 | terser({ 36 | sourcemap: false 37 | }) 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/spadille/utils/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint 4 | no-new-func: off, 5 | semi: off */ 6 | 7 | const isBrowser = new Function(` 8 | try { 9 | return ( 10 | typeof window !== 'undefined' && 11 | this === window && 12 | this.window === this && 13 | typeof window.crypto !== 'undefined' && 14 | typeof window.crypto.subtle !== 'undefined' && 15 | ({}).toString.call(this) === '[object Window]' 16 | ); 17 | } catch (_) { 18 | return false; 19 | } 20 | `); 21 | 22 | // TODO: rethink that better later 23 | const isNode = new Function(` 24 | try { 25 | return ( 26 | typeof window === 'undefined' || 27 | this !== window || 28 | this.window !== this || 29 | typeof window.crypto === 'undefined' || 30 | typeof window.crypto.subtle === 'undefined' || 31 | ({}).toString.call(this) !== '[object Window]' 32 | ); 33 | } catch (_) { 34 | return false; 35 | } 36 | `); 37 | 38 | module.exports.isBrowser = isBrowser; 39 | module.exports.isNode = isNode; 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Marco Aurélio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /types/spadille.d.ts: -------------------------------------------------------------------------------- 1 | export = spadille; 2 | 3 | interface PRNGOptions { 4 | secret: string, 5 | payload: string, 6 | minimum: number, 7 | maximum: number, 8 | amount: number, 9 | distinct: boolean 10 | } 11 | 12 | interface PermutationOptions { 13 | secret: string, 14 | payload: string, 15 | inputSequence: T[] 16 | } 17 | 18 | interface PickOptions { 19 | secret: string; 20 | payload: string; 21 | sequence: T[]; 22 | amount?: number; 23 | distinct?: boolean; 24 | } 25 | 26 | declare const spadille: { 27 | lottery: { 28 | brazillian: { 29 | federal: (secret: string, payload: string) => Promise; 30 | megaSena: (secret: string, payload: string) => Promise; 31 | }; 32 | }; 33 | prng: { 34 | generate: (options: PRNGOptions) => Promise; 35 | rand: (secret: string, payload: string) => Promise; 36 | pick:(options: PickOptions) => Promise; 37 | permute:(options: PermutationOptions) => Promise; 38 | }; 39 | secret: { 40 | generate: (bytes: number) => Promise; 41 | }; 42 | base64: { 43 | encode: (binary: string) => string; 44 | decode: (data: string) => string; 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/spadille/hmac/browser.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | 'use strict'; 4 | 5 | /* eslint-env browser */ 6 | /* eslint 7 | semi: off */ 8 | 9 | module.exports.init = function () { 10 | const crypto = window.crypto.subtle; 11 | const encoder = new TextEncoder('utf-8'); 12 | const HASH = 'SHA-512'; 13 | 14 | const num2hex = function (num) { 15 | return ('00' + num.toString(16)).slice(-2); 16 | }; 17 | 18 | const bin2hex = function (payload) { 19 | const array = new Uint8Array(payload); 20 | return [...array].map(num2hex).join(''); 21 | }; 22 | 23 | const str2arr = function (str) { 24 | return encoder.encode(str); 25 | }; 26 | 27 | const importKey = async function (secret, target) { 28 | const secretParams = str2arr(secret); 29 | const keyOptions = { 30 | name: 'HMAC', 31 | hash: { name: HASH } 32 | }; 33 | const key = await crypto.importKey('raw', secretParams, keyOptions, false, [target]); 34 | return key; 35 | }; 36 | 37 | const hmac = async function (secret, message) { 38 | const payload = str2arr(message); 39 | const key = await importKey(secret, 'sign'); 40 | const signature = await crypto.sign('HMAC', key, payload); 41 | return bin2hex(signature); 42 | }; 43 | 44 | return { hmac }; 45 | }; 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Vim files 9 | *.swp 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | _esy/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # Jekyll files 65 | docs/.bundle/ 66 | docs/vendor/ 67 | docs/_site/ 68 | 69 | # next.js build output 70 | .next 71 | -------------------------------------------------------------------------------- /src/spadille/base64/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env browser, node */ 4 | /* eslint 5 | semi: off */ 6 | 7 | const { 8 | isNode, isBrowser 9 | } = require('../utils/environment'); 10 | 11 | let encoding = null; 12 | 13 | const wrapErrors = function (encoding) { 14 | const result = {}; 15 | 16 | result.toBase64 = function (binary) { 17 | /* istanbul ignore next */ 18 | try { 19 | if (!binary) { 20 | throw Error('< error ignored >') 21 | } 22 | const payload = encoding.toBase64(binary); 23 | if (payload) { 24 | return payload 25 | } 26 | throw Error('< error ignored >'); 27 | } catch (_reason) { 28 | throw Error('Failed to encode binary data as base-64!'); 29 | } 30 | }; 31 | 32 | result.fromBase64 = function (payload) { 33 | /* istanbul ignore next */ 34 | try { 35 | if (!payload) { 36 | throw Error('< error ignored >') 37 | } 38 | const binary = encoding.fromBase64(payload); 39 | if (binary) { 40 | return binary; 41 | } 42 | throw Error('< error ignored >'); 43 | } catch (_reason) { 44 | throw Error('Failed to decode base-64 data into binary!'); 45 | } 46 | }; 47 | 48 | return result; 49 | }; 50 | 51 | (function () { 52 | /* istanbul ignore next */ 53 | if (isBrowser()) { 54 | encoding = require('./browser').init(); 55 | } else if (isNode()) { 56 | encoding = require('./node').init(); 57 | } else { 58 | throw Error('Could not detect execution context!'); 59 | } 60 | })(); 61 | 62 | encoding = wrapErrors(encoding) 63 | module.exports.encode = encoding.toBase64; 64 | module.exports.decode = encoding.fromBase64; 65 | -------------------------------------------------------------------------------- /__tests__/bias.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, es6, jest */ 2 | 3 | 'use strict' 4 | 5 | const cuid = require('cuid') 6 | const support = require('../support') 7 | const SECRET = 'FAKE NEWS!' 8 | 9 | beforeAll(support.setup) 10 | 11 | // WARNING: non-deterministic test, might fail sometimes, 12 | // has minimum acceptance threshold of ocurrence/probability 13 | // for every event in the given interval of points 14 | const measureBias = async function (options) { 15 | const { MINIMUM, MAXIMUM, THRESHOLD } = options 16 | const ELEMENTS = MAXIMUM - MINIMUM 17 | const ITERATIONS = ELEMENTS * (1 / THRESHOLD) 18 | const generated = [] 19 | 20 | for (let index = 0; index < ELEMENTS; index += 1) { 21 | generated[index] = false 22 | } 23 | 24 | for (let index = 0; index < ITERATIONS; index += 1) { 25 | const payload = cuid() 26 | 27 | const sequence = await support.call( 28 | function (secret, payload, minimum, maximum) { 29 | return this.prng.generate({ 30 | secret, 31 | payload, 32 | minimum: minimum, 33 | maximum: maximum, 34 | amount: 1, 35 | distinct: false 36 | }) 37 | }, 38 | SECRET, 39 | payload, 40 | MINIMUM, 41 | MAXIMUM 42 | ) 43 | 44 | const index = sequence[0] - MINIMUM 45 | generated[index] = true 46 | } 47 | 48 | const nonBiased = generated.reduce((left, right) => { 49 | return left && right 50 | }, true) 51 | 52 | return !nonBiased 53 | } 54 | 55 | it('should generate non-biased numbers', async function () { 56 | expect.assertions(1) 57 | 58 | const biased = await measureBias({ 59 | MINIMUM: 1, 60 | MAXIMUM: 6, 61 | THRESHOLD: 0.2 62 | }) 63 | 64 | expect(biased).toBe(false) 65 | }) 66 | 67 | it('should generate non-biased huge numbers', async function () { 68 | expect.assertions(1) 69 | 70 | const biased = await measureBias({ 71 | MINIMUM: 49999955, 72 | MAXIMUM: 50000000, 73 | THRESHOLD: 0.1 74 | }) 75 | 76 | expect(biased).toBe(false) 77 | }, 10000) 78 | -------------------------------------------------------------------------------- /src/spadille/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env node, browser */ 4 | /* eslint 5 | semi: off */ 6 | 7 | const hmac = require('../hmac'); 8 | 9 | const cycle = function (number, minimum, maximum) { 10 | const limit = (maximum + 1) - minimum; 11 | return minimum + (number % limit); 12 | }; 13 | 14 | const intXor = function (left, right) { 15 | return left ^ right; 16 | }; 17 | 18 | const fromHex = function (hex) { 19 | return Number.parseInt(hex, 16); 20 | }; 21 | 22 | const toHex = function (number) { 23 | return number.toString(16); 24 | } 25 | 26 | const generatePiece = async function (hash, suffix) { 27 | const image = await hmac.sign(hash, suffix.toString()); 28 | const subPieces = []; 29 | 30 | for (let index = 0; index < 64; index += 1) { 31 | subPieces.push(fromHex(image.substr(index * 8, 8))); 32 | } 33 | 34 | return toHex(subPieces.reduce(intXor)); 35 | }; 36 | 37 | const computeNumber = function (randomGen, data) { 38 | return randomGen(Math.abs(fromHex(data))); 39 | }; 40 | 41 | const splitInPieces = async function (text /*, amount */) { 42 | const stream = { 43 | index: -1, 44 | text: text, 45 | generate: function () { 46 | this.index += 1; 47 | return generatePiece(this.text, this.index); 48 | } 49 | }; 50 | 51 | return stream; 52 | }; 53 | 54 | const makeRandomGen = function (minimum, maximum) { 55 | return function (seed) { 56 | return cycle(seed, minimum, maximum); 57 | }; 58 | }; 59 | 60 | const missing = function (value) { 61 | return value === undefined || value === null; 62 | }; 63 | 64 | const option = function (value, defaultValue) { 65 | return missing(value) ? defaultValue : value; 66 | }; 67 | 68 | const sortOrder = function (x, y) { 69 | /* istanbul ignore next */ 70 | return x > y ? 1 : x < y ? -1 : 0; 71 | }; 72 | 73 | const sortArrayNumber = function (array) { 74 | return array.sort(sortOrder); 75 | }; 76 | 77 | module.exports.cycle = cycle; 78 | module.exports.splitInPieces = splitInPieces; 79 | module.exports.fromHex = fromHex; 80 | module.exports.makeRandomGen = makeRandomGen; 81 | module.exports.missing = missing; 82 | module.exports.option = option; 83 | module.exports.sortArrayNumber = sortArrayNumber; 84 | module.exports.computeNumber = computeNumber; 85 | -------------------------------------------------------------------------------- /support/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env node, es6, jest */ 4 | /* eslint 5 | no-new-func: off */ 6 | /* global page */ 7 | 8 | const LIB_DIR = process.env.LIB_DIR || 'src/spadille/index.js' 9 | 10 | if (process.env.BROWSER_ENV) { 11 | const fs = require('fs') 12 | const script = fs.readFileSync(`./${LIB_DIR}`) 13 | 14 | module.exports.setup = async function () { 15 | await page.setJavaScriptEnabled(true) 16 | await page.goto('file:///dev/null') // this make a "secure context" for crypto.subtle API 17 | // await page.addScriptTag({ content: script }) 18 | 19 | return true 20 | } 21 | 22 | module.exports.call = async function (closure, ...values) { 23 | /* 24 | const prefix = '\t' + script + ';\n\tvar block = ' 25 | const suffix = ';\n\ttry { return await block.bind(spadille)(...arguments); } catch(reason) { throw reason.message; }\n' 26 | const body = '(async function () {' + prefix + closure.toString() + suffix + '})(...arguments)' 27 | const modifiedClosure = `function () { return ${body}; }` 28 | */ 29 | 30 | const modifiedClosure = new Function(` 31 | ${script} 32 | const block = (${closure.toString()}); 33 | return block.bind(spadille)(...arguments) 34 | .then(function (result) { 35 | return { 36 | status: 'RESOLVED', 37 | value: result 38 | }; 39 | }) 40 | .catch(function (reason) { 41 | return { 42 | status: 'REJECTED', 43 | message: reason.message, 44 | }; 45 | }); 46 | `) 47 | 48 | try { 49 | const result = await page.evaluate(modifiedClosure, ...values) 50 | if (result.status === 'REJECTED') { 51 | throw result 52 | } 53 | return result.value 54 | } catch (reason) { 55 | throw Error( 56 | reason.message 57 | .replace(/Evaluation failed: /, '') 58 | .replace(/Error: /, '') 59 | ) 60 | } 61 | } 62 | } else { 63 | module.exports.setup = async function () { 64 | require(`../${LIB_DIR}`) // test if loads OK 65 | return true 66 | } 67 | 68 | module.exports.call = function (closure, ...values) { 69 | const __spadille__ = require(`../${LIB_DIR}`) 70 | return closure.bind(__spadille__)(...values) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /__tests__/secret.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, es6, jest */ 2 | 3 | 'use strict' 4 | 5 | const support = require('../support') 6 | 7 | beforeAll(support.setup) 8 | 9 | const newSecret = function (length) { 10 | return support.call(function (length) { 11 | return this.secret.generate(length) 12 | }, length) 13 | } 14 | 15 | const encode = function (binary) { 16 | return support.call(function (binary) { 17 | const spadille = this 18 | return new Promise(function (resolve, reject) { 19 | try { 20 | resolve(spadille.base64.encode(binary)) 21 | } catch (reason) { 22 | reject(reason) 23 | } 24 | }) 25 | }, binary) 26 | } 27 | 28 | const decode = function (data) { 29 | return support.call(function (data) { 30 | const spadille = this 31 | return new Promise(function (resolve, reject) { 32 | try { 33 | resolve(spadille.base64.decode(data)) 34 | } catch (reason) { 35 | reject(reason) 36 | } 37 | }) 38 | }, data) 39 | } 40 | 41 | it('should generate a random secret', async function () { 42 | expect.assertions(44) 43 | 44 | for (let length = 4; length < 26; length += 1) { 45 | const secret = await newSecret(length) 46 | expect(secret.length).toBe(length) 47 | expect(secret).toBe(secret.toString()) 48 | } 49 | }) 50 | 51 | it('should base-64 convert back and forth the secret', async function () { 52 | expect.assertions(3) 53 | 54 | const secret = await newSecret(16) 55 | const base64 = await encode(secret) 56 | 57 | expect(Buffer.from(secret, 'utf-8').toString('base64')).toBe(base64) 58 | expect( 59 | Buffer.from( 60 | unescape(encodeURIComponent(secret)), 61 | 'ascii' 62 | ).toString('base64') 63 | ).toBe(base64) 64 | await expect(decode(base64)).resolves.toBe(secret) 65 | }) 66 | 67 | it('should fail to encode/decode invalid base64 data', async function () { 68 | expect.assertions(3) 69 | 70 | await expect(decode('*&@%@#$%')).rejects.toMatchObject({ 71 | message: 'Failed to decode base-64 data into binary!' 72 | }) 73 | 74 | await expect(encode('')).rejects.toMatchObject({ 75 | message: 'Failed to encode binary data as base-64!' 76 | }) 77 | 78 | await expect(decode('')).rejects.toMatchObject({ 79 | message: 'Failed to decode base-64 data into binary!' 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /__tests__/lottery.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, es6, jest */ 2 | 3 | 'use strict' 4 | 5 | const cuid = require('cuid') 6 | const support = require('../support') 7 | const SECRET = 'OH NO!' 8 | 9 | beforeAll(support.setup) 10 | 11 | it('should generate a mega sena sequence', async function () { 12 | expect.assertions(22) 13 | 14 | const payload = cuid() 15 | 16 | const sequence = await support.call(function (secret, payload) { 17 | return this.lottery.brazillian.megaSena(secret, payload) 18 | }, SECRET, payload) 19 | 20 | const uniqueness = {} 21 | 22 | sequence.forEach(function (number) { 23 | expect(number).toBeGreaterThanOrEqual(1) 24 | expect(number).toBe(Number.parseInt(number)) 25 | expect(number).toBeLessThanOrEqual(60) 26 | uniqueness[number.toString()] = true 27 | }) 28 | 29 | expect(sequence.length).toBe(Object.keys(uniqueness).length) 30 | expect(sequence.length).toBe(6) 31 | 32 | const sequenceCopy = await support.call(function (secret, payload) { 33 | return this.lottery.brazillian.megaSena(secret, payload) 34 | }, SECRET, payload) 35 | 36 | expect(sequenceCopy).toStrictEqual(sequence) 37 | 38 | const orderedSequence = sequence.sort(function (x, y) { 39 | return x - y 40 | }) 41 | 42 | expect(orderedSequence).toStrictEqual(sequence) 43 | }) 44 | 45 | it('should match the fixed mega-sena sequence', async function () { 46 | expect.assertions(1) 47 | 48 | const fixedSequence = [1, 13, 18, 22, 55, 60] 49 | const payload = '[fixed payload data]' 50 | 51 | const sequence = await support.call(function (secret, payload) { 52 | return this.lottery.brazillian.megaSena(secret, payload) 53 | }, SECRET, payload) 54 | 55 | expect(sequence).toStrictEqual(fixedSequence) 56 | }) 57 | 58 | it('should generate a federal sequence', async function () { 59 | expect.assertions(17) 60 | 61 | const payload = cuid() 62 | 63 | const sequence = await support.call(function (secret, payload) { 64 | return this.lottery.brazillian.federal(secret, payload) 65 | }, SECRET, payload) 66 | 67 | const sequenceCopy = await support.call(function (secret, payload) { 68 | return this.lottery.brazillian.federal(secret, payload) 69 | }, SECRET, payload) 70 | 71 | expect(sequenceCopy).toStrictEqual(sequence) 72 | expect(sequence.length).toBe(5) 73 | 74 | sequence.split('').map(Number).forEach(function (number) { 75 | expect(number).toBeGreaterThanOrEqual(0) 76 | expect(number).toBe(Number.parseInt(number)) 77 | expect(number).toBeLessThanOrEqual(9) 78 | }) 79 | }) 80 | 81 | it('should match the fixed federal sequence', async function () { 82 | expect.assertions(1) 83 | 84 | const fixedSequence = '74901' 85 | const payload = '[fixed payload data]' 86 | 87 | const sequence = await support.call(function (secret, payload) { 88 | return this.lottery.brazillian.federal(secret, payload) 89 | }, SECRET, payload) 90 | 91 | expect(sequence).toStrictEqual(fixedSequence) 92 | }) 93 | -------------------------------------------------------------------------------- /src/spadille/prng/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-env node */ 4 | /* eslint 5 | comma-dangle: off, 6 | semi: off */ 7 | 8 | const hmac = require('../hmac'); 9 | const { 10 | makeRandomGen, 11 | option, 12 | splitInPieces, 13 | computeNumber, 14 | } = require('../utils'); 15 | 16 | const generate = async function (options) { 17 | let { secret, payload, minimum, maximum, amount, distinct } = options; 18 | 19 | minimum = option(minimum, 1); 20 | maximum = option(maximum, 60); 21 | amount = option(amount, 6); 22 | distinct = option(distinct, true); 23 | 24 | // interval is inclusive 25 | if (distinct && amount > maximum - minimum + 1) { 26 | throw Error( 27 | 'The number of balls [amount] must not be greater than the [(maximum - minimum) + 1] number of RNG when [distinct] flag is on!', 28 | ); 29 | } 30 | 31 | const seed = await hmac.sign(secret, payload); 32 | const randomGen = makeRandomGen(minimum, maximum); 33 | 34 | const stream = await splitInPieces(seed); 35 | const result = []; 36 | 37 | if (distinct) { 38 | const cache = {}; 39 | let index = 0; 40 | while (index < amount) { 41 | const data = await stream.generate(); 42 | const number = computeNumber(randomGen, data); 43 | 44 | // no detected duplicate 45 | if (!cache[number.toString()]) { 46 | cache[number.toString()] = true; 47 | index += 1; 48 | result.push(number); 49 | } 50 | } 51 | } else { 52 | for (let index = 0; index < amount; index += 1) { 53 | const data = await stream.generate(); 54 | const number = computeNumber(randomGen, data); 55 | result.push(number); 56 | } 57 | } 58 | return result; 59 | }; 60 | 61 | const permute = async function (options) { 62 | const { secret, payload, inputSequence } = options; 63 | 64 | const ordering = await generate({ 65 | secret, 66 | payload, 67 | minimum: 0, 68 | maximum: inputSequence.length - 1, 69 | amount: inputSequence.length, 70 | distinct: true, 71 | }); 72 | 73 | return ordering.map(index => inputSequence[index]); 74 | }; 75 | 76 | const pick = async function (options) { 77 | const { secret, payload, sequence } = options; 78 | const distinct = option(options.distinct, false); 79 | const amount = option(options.amount, 1); 80 | 81 | const indexes = await generate({ 82 | secret, 83 | payload, 84 | minimum: 0, 85 | maximum: sequence.length - 1, 86 | amount, 87 | distinct, 88 | }); 89 | 90 | return indexes.map(index => sequence[index]); 91 | }; 92 | 93 | const rand = async function (secret, payload) { 94 | const maximumInt = Math.pow(2, 31); 95 | 96 | const [randomInt] = await generate({ 97 | secret, 98 | payload, 99 | minimum: 0, 100 | maximum: maximumInt - 1, 101 | amount: 1, 102 | }); 103 | 104 | return randomInt / maximumInt; 105 | }; 106 | 107 | module.exports.generate = generate; 108 | module.exports.permute = permute; 109 | module.exports.pick = pick; 110 | module.exports.rand = rand; 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spadille", 3 | "version": "0.0.3", 4 | "description": "Verifiable/deterministic fair tickets generation for lotteries, raffles and gambling games.", 5 | "main": "build/spadille/index.js", 6 | "browser": "dist/index.js", 7 | "types": "types/spadille.d.ts", 8 | "scripts": { 9 | "build:back": "babel src -d build --minified --no-comments --compact", 10 | "build": "yarn run build:back && yarn run build:front", 11 | "build:front": "rollup -c --environment INCLUDE_DEPS,BUILD:production", 12 | "lint": "standard --verbose", 13 | "pretest": "yarn run lint && yarn run build", 14 | "test:back": "LIB_DIR=build/spadille/index.js jest --verbose", 15 | "test:front": "LIB_DIR=dist/index.js BROWSER_ENV=1 jest --verbose --preset jest-puppeteer", 16 | "test": "yarn run test:back && yarn run test:front", 17 | "precoverage": "yarn run test", 18 | "coverage": "LIB_DIR=src/spadille jest --coverage", 19 | "prereport": "yarn run coverage", 20 | "report": "cat coverage/lcov.info | coveralls", 21 | "jekyll:setup": "cd docs && bundle install --path vendor/bundle && cd ..", 22 | "jekyll:build": "cp README.md docs/index.md", 23 | "prejekyll:serve": "yarn run jekyll:build", 24 | "jekyll:serve": "cd docs && bundle exec jekyll serve && cd ..", 25 | "prepublish": "yarn run build" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/marcoonroad/spadille.git" 30 | }, 31 | "keywords": [ 32 | "gambling", 33 | "provably-fair", 34 | "secure-multi-party-computation", 35 | "commitment-schemes", 36 | "random-number-generators", 37 | "pseudo-random-generator", 38 | "brazillian-lotteries", 39 | "casino", 40 | "betting", 41 | "javascript", 42 | "lottery", 43 | "lotteries", 44 | "raffle", 45 | "raffles", 46 | "cryptography", 47 | "hmac", 48 | "web-crypto", 49 | "web-cryptography-api", 50 | "rng", 51 | "prng" 52 | ], 53 | "author": "Marco Aurélio da Silva (marcoonroad@gmail.com)", 54 | "license": "MIT", 55 | "bugs": { 56 | "url": "https://github.com/marcoonroad/spadille/issues" 57 | }, 58 | "jest": { 59 | "coveragePathIgnorePatterns": [ 60 | "./node_modules/", 61 | "./support/", 62 | "./TRASH/" 63 | ], 64 | "collectCoverageFrom": [ 65 | "**/*.{js,jsx}", 66 | "!**/node_modules/**", 67 | "!**/support/**", 68 | "!**/dist/**", 69 | "!**/build/**", 70 | "!**/docs/**", 71 | "!**/coverage/**", 72 | "!**/rollup.config.js" 73 | ] 74 | }, 75 | "standard": { 76 | "ignore": [ 77 | "dist/", 78 | "build/", 79 | "node_modules/" 80 | ], 81 | "parser": "babel-eslint" 82 | }, 83 | "homepage": "https://spadille.marcoonroad.dev", 84 | "devDependencies": { 85 | "babel-cli": "^6.26.0", 86 | "babel-eslint": "^10.0.2", 87 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 88 | "babel-preset-env": "^1.7.0", 89 | "babel-preset-latest": "^6.24.1", 90 | "coveralls": "^3.0.2", 91 | "cuid": "^2.1.6", 92 | "eslint": "^6.0.1", 93 | "jest": "^23.6.0", 94 | "jest-puppeteer": "^4.2.0", 95 | "jsdom": "^13.2.0", 96 | "puppeteer": "^1.18.1", 97 | "rollup": "^1.1.2", 98 | "rollup-plugin-commonjs": "^9.2.0", 99 | "rollup-plugin-node-builtins": "^2.1.2", 100 | "rollup-plugin-node-globals": "^1.4.0", 101 | "rollup-plugin-node-resolve": "^4.0.0", 102 | "rollup-plugin-terser": "^4.0.4", 103 | "standard": "^12.0.1" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /__tests__/prng.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, es6, jest */ 2 | 3 | 'use strict' 4 | 5 | const cuid = require('cuid') 6 | const support = require('../support') 7 | const SECRET = 'YAY!' 8 | 9 | beforeAll(support.setup) 10 | 11 | it('should generate arbitrary/huge luck numbers', async function () { 12 | expect.assertions(1501) 13 | 14 | const payload = cuid() 15 | 16 | const sequence = await support.call( 17 | function (secret, payload) { 18 | return this.prng.generate({ 19 | secret, 20 | payload, 21 | minimum: 30, 22 | maximum: 9000, 23 | amount: 500, 24 | distinct: false 25 | }) 26 | }, 27 | SECRET, 28 | payload 29 | ) 30 | 31 | sequence.forEach(function (number) { 32 | expect(number).toBeGreaterThanOrEqual(30) 33 | expect(number).toBe(Number.parseInt(number)) 34 | expect(number).toBeLessThanOrEqual(9000) 35 | }) 36 | 37 | expect(sequence.length).toBe(500) 38 | }) 39 | 40 | it('should not enter in infinite loop while', async function () { 41 | expect.assertions(1) 42 | 43 | const payload = cuid() 44 | 45 | const failed = await support.call( 46 | function (secret, payload) { 47 | return this.prng 48 | .generate({ 49 | secret, 50 | payload, 51 | minimum: 7, 52 | maximum: 10, 53 | amount: 5, 54 | distinct: true 55 | }) 56 | .then(function () { 57 | return false 58 | }) 59 | .catch(function (reason) { 60 | return ( 61 | reason.message === 62 | 'The number of balls [amount] must not be greater than the [(maximum - minimum) + 1] number of RNG when [distinct] flag is on!' 63 | ) 64 | }) 65 | }, 66 | SECRET, 67 | payload 68 | ) 69 | 70 | expect(failed).toBe(true) 71 | }) 72 | 73 | it('should pass if the interval is the same of amount when distinct enabled', async function () { 74 | expect.assertions(151) 75 | 76 | const payload = cuid() 77 | 78 | const sequence = await support.call( 79 | function (secret, payload) { 80 | return this.prng.generate({ 81 | secret, 82 | payload, 83 | minimum: 11, 84 | maximum: 60, 85 | amount: 50, 86 | distinct: true 87 | }) 88 | }, 89 | SECRET, 90 | payload 91 | ) 92 | 93 | sequence.forEach(function (number) { 94 | expect(number).toBeGreaterThanOrEqual(11) 95 | expect(number).toBe(Number.parseInt(number)) 96 | expect(number).toBeLessThanOrEqual(60) 97 | }) 98 | 99 | expect(sequence.length).toBe(50) 100 | }, 35000) 101 | 102 | it('should shuffle a list', async function () { 103 | expect.assertions(22) 104 | 105 | const payload = cuid() 106 | 107 | const inputSequence = [1, 2, 3, 4, 5, 6, 7] 108 | const outputSequence = await support.call( 109 | function (secret, payload, inputSequence) { 110 | return this.prng.permute({ 111 | secret, 112 | payload, 113 | inputSequence 114 | }) 115 | }, 116 | SECRET, 117 | payload, 118 | inputSequence 119 | ) 120 | 121 | outputSequence.forEach(function (number) { 122 | expect(number).toBeGreaterThanOrEqual(1) 123 | expect(number).toBe(Number.parseInt(number)) 124 | expect(number).toBeLessThanOrEqual(7) 125 | }) 126 | 127 | expect(outputSequence.length).toBe(7) 128 | }) 129 | 130 | it('should generate a random rational number [>= 0] and [< 1]', async function () { 131 | expect.assertions(2) 132 | 133 | const payload = cuid() 134 | 135 | const fraction = await support.call( 136 | function (secret, payload) { 137 | return this.prng.rand(secret, payload) 138 | }, 139 | SECRET, 140 | payload 141 | ) 142 | 143 | expect(fraction).toBeGreaterThanOrEqual(0) 144 | expect(fraction).toBeLessThan(1) 145 | }) 146 | 147 | it('should pick a random element from given set', async function () { 148 | expect.assertions(1) 149 | 150 | const secret = '' 151 | const payload = '{requestId:16}' 152 | const fruits = ['apple', 'banana', 'orange'] 153 | const [fruit] = await support.call( 154 | function (secret, payload, sequence) { 155 | return this.prng.pick({ secret, payload, sequence }) 156 | }, 157 | secret, 158 | payload, 159 | fruits 160 | ) 161 | 162 | expect(fruit).toBe('apple') 163 | }) 164 | 165 | it('should make pick=permute when distinct=true & amount=len(seq)', async function () { 166 | expect.assertions(1) 167 | 168 | const payload = cuid() 169 | const sequence = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] 170 | 171 | const permuteOutput = await support.call( 172 | function (secret, payload, inputSequence) { 173 | return this.prng.permute({ secret, payload, inputSequence }) 174 | }, 175 | SECRET, 176 | payload, 177 | sequence 178 | ) 179 | 180 | const pickOutput = await support.call( 181 | function (secret, payload, sequence) { 182 | return this.prng.pick({ 183 | secret, 184 | payload, 185 | sequence, 186 | amount: sequence.length, 187 | distinct: true 188 | }) 189 | }, 190 | SECRET, 191 | payload, 192 | sequence 193 | ) 194 | 195 | expect(pickOutput).toEqual(permuteOutput) 196 | }) 197 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | var spadille=function(n,t){"use strict";t=t&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t;var e={isBrowser:new Function("\n try {\n return (\n typeof window !== 'undefined' &&\n this === window &&\n this.window === this &&\n typeof window.crypto !== 'undefined' &&\n typeof window.crypto.subtle !== 'undefined' &&\n ({}).toString.call(this) === '[object Window]'\n );\n } catch (_) {\n return false;\n }\n"),isNode:new Function("\n try {\n return (\n typeof window === 'undefined' ||\n this !== window ||\n this.window !== this ||\n typeof window.crypto === 'undefined' ||\n typeof window.crypto.subtle === 'undefined' ||\n ({}).toString.call(this) !== '[object Window]'\n );\n } catch (_) {\n return false;\n }\n")},r=function(){const n=window.crypto.subtle,t=new TextEncoder("utf-8"),e=function(n){return("00"+n.toString(16)).slice(-2)},r=function(n){return t.encode(n)};return{hmac:async function(t,o){const i=r(o),a=await async function(t,e){const o=r(t),i={name:"HMAC",hash:{name:"SHA-512"}};return await n.importKey("raw",o,i,!1,[e])}(t,"sign");return function(n){return[...new Uint8Array(n)].map(e).join("")}(await n.sign("HMAC",a,i))}}},o=function(){const n=t;return{hmac:async function(t,e){const r=n.createHmac("sha512",t);return r.update(e),r.digest("hex")}}};const{isNode:i,isBrowser:a}=e;let c=null;!function(){if(a())c=r().hmac;else{if(!i())throw Error("Could not detect execution context!");c=o().hmac}}();var u={sign:c};const s=function(n,t,e){return t+n%(e+1-t)},f=function(n,t){return n^t},d=function(n){return Number.parseInt(n,16)},m=function(n){return null==n},w=function(n,t){return n>t?1:no-r+1)throw Error("The number of balls [amount] must not be greater than the [(maximum - minimum) + 1] number of RNG when [distinct] flag is on!");const c=await u.sign(t,e),s=y(r,o),f=await h(c),d=[];if(a){const n={};let t=0;for(;tr[n])},pick:async function(n){const{secret:t,payload:e,sequence:r}=n,o=p(n.distinct,!1),i=p(n.amount,1);return(await b({secret:t,payload:e,minimum:0,maximum:r.length-1,amount:i,distinct:o})).map(n=>r[n])},rand:async function(n,t){const e=Math.pow(2,31),[r]=await b({secret:n,payload:t,minimum:0,maximum:e-1,amount:1});return r/e}};var S={brazillian:{megaSena:async function(n,t){const e=await x.generate({secret:n,payload:t});return l.sortArrayNumber(e)},federal:async function(n,t){return(await x.generate({secret:n,payload:t,minimum:0,maximum:9,amount:5,distinct:!1})).map(function(n){return n.toString()}).join("")}}};var B={fromString:function(n){const t=(new TextEncoder).encode(n),e=JSON.parse(`[${t.toString()}]`),r=new Uint8Array(e.length);return e.forEach(function(n,t){r[t]=n}),r},toString:function(n,t){return t=t||"utf-8",new TextDecoder(t).decode(n)}},E=function(){const n=B,t=window.crypto;return{secret:async function(e){const r=t.getRandomValues(new Uint8Array(e));return n.toString(r,"ascii")}}},N=function(){const n=t;return{secret:async function(t){return n.randomBytes(t).toString("ascii")}}};const{isNode:v,isBrowser:A}=e;let F=null;!function(){if(A())F=E().secret;else{if(!v())throw Error("Could not detect execution context!");F=N().secret}}();var C={generate:F},R=function(){return{toBase64:new Function("return function (binary) {\n return btoa (unescape (encodeURIComponent (binary)));\n }")(),fromBase64:new Function("return function (base64) {\n return decodeURIComponent (escape (atob (base64)));\n }")()}},j=function(){return{toBase64:new Function("return function (binary) {\n return Buffer.from(binary, 'utf-8').toString('base64');\n };")(),fromBase64:new Function("return function (payload) {\n return Buffer.from(payload, 'base64').toString('utf-8');\n };")()}};const{isNode:H,isBrowser:I}=e;let U=null;!function(){if(I())U=R();else{if(!H())throw Error("Could not detect execution context!");U=j()}}();var M=(U=function(n){const t={toBase64:function(t){try{if(!t)throw Error("< error ignored >");const e=n.toBase64(t);if(e)return e;throw Error("< error ignored >")}catch(n){throw Error("Failed to encode binary data as base-64!")}},fromBase64:function(t){try{if(!t)throw Error("< error ignored >");const e=n.fromBase64(t);if(e)return e;throw Error("< error ignored >")}catch(n){throw Error("Failed to decode base-64 data into binary!")}}};return t}(U)).toBase64,T=U.fromBase64,k=S,G=x,O=C,P={encode:M,decode:T},q={lottery:k,prng:G,secret:O,base64:P};return n.base64=P,n.default=q,n.lottery=k,n.prng=G,n.secret=O,n}({},crypto); -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (6.0.6) 5 | concurrent-ruby (~> 1.0, >= 1.0.2) 6 | i18n (>= 0.7, < 2) 7 | minitest (~> 5.1) 8 | tzinfo (~> 1.1) 9 | zeitwerk (~> 2.2, >= 2.2.2) 10 | addressable (2.8.1) 11 | public_suffix (>= 2.0.2, < 6.0) 12 | coffee-script (2.4.1) 13 | coffee-script-source 14 | execjs 15 | coffee-script-source (1.11.1) 16 | colorator (1.1.0) 17 | commonmarker (0.23.6) 18 | concurrent-ruby (1.1.10) 19 | dnsruby (1.61.9) 20 | simpleidn (~> 0.1) 21 | em-websocket (0.5.3) 22 | eventmachine (>= 0.12.9) 23 | http_parser.rb (~> 0) 24 | ethon (0.15.0) 25 | ffi (>= 1.15.0) 26 | eventmachine (1.2.7) 27 | execjs (2.8.1) 28 | faraday (2.6.0) 29 | faraday-net_http (>= 2.0, < 3.1) 30 | ruby2_keywords (>= 0.0.4) 31 | faraday-net_http (3.0.1) 32 | ffi (1.15.5) 33 | forwardable-extended (2.6.0) 34 | gemoji (3.0.1) 35 | github-pages (227) 36 | github-pages-health-check (= 1.17.9) 37 | jekyll (= 3.9.2) 38 | jekyll-avatar (= 0.7.0) 39 | jekyll-coffeescript (= 1.1.1) 40 | jekyll-commonmark-ghpages (= 0.2.0) 41 | jekyll-default-layout (= 0.1.4) 42 | jekyll-feed (= 0.15.1) 43 | jekyll-gist (= 1.5.0) 44 | jekyll-github-metadata (= 2.13.0) 45 | jekyll-include-cache (= 0.2.1) 46 | jekyll-mentions (= 1.6.0) 47 | jekyll-optional-front-matter (= 0.3.2) 48 | jekyll-paginate (= 1.1.0) 49 | jekyll-readme-index (= 0.3.0) 50 | jekyll-redirect-from (= 0.16.0) 51 | jekyll-relative-links (= 0.6.1) 52 | jekyll-remote-theme (= 0.4.3) 53 | jekyll-sass-converter (= 1.5.2) 54 | jekyll-seo-tag (= 2.8.0) 55 | jekyll-sitemap (= 1.4.0) 56 | jekyll-swiss (= 1.0.0) 57 | jekyll-theme-architect (= 0.2.0) 58 | jekyll-theme-cayman (= 0.2.0) 59 | jekyll-theme-dinky (= 0.2.0) 60 | jekyll-theme-hacker (= 0.2.0) 61 | jekyll-theme-leap-day (= 0.2.0) 62 | jekyll-theme-merlot (= 0.2.0) 63 | jekyll-theme-midnight (= 0.2.0) 64 | jekyll-theme-minimal (= 0.2.0) 65 | jekyll-theme-modernist (= 0.2.0) 66 | jekyll-theme-primer (= 0.6.0) 67 | jekyll-theme-slate (= 0.2.0) 68 | jekyll-theme-tactile (= 0.2.0) 69 | jekyll-theme-time-machine (= 0.2.0) 70 | jekyll-titles-from-headings (= 0.5.3) 71 | jemoji (= 0.12.0) 72 | kramdown (= 2.3.2) 73 | kramdown-parser-gfm (= 1.1.0) 74 | liquid (= 4.0.3) 75 | mercenary (~> 0.3) 76 | minima (= 2.5.1) 77 | nokogiri (>= 1.13.6, < 2.0) 78 | rouge (= 3.26.0) 79 | terminal-table (~> 1.4) 80 | github-pages-health-check (1.17.9) 81 | addressable (~> 2.3) 82 | dnsruby (~> 1.60) 83 | octokit (~> 4.0) 84 | public_suffix (>= 3.0, < 5.0) 85 | typhoeus (~> 1.3) 86 | html-pipeline (2.14.3) 87 | activesupport (>= 2) 88 | nokogiri (>= 1.4) 89 | http_parser.rb (0.8.0) 90 | i18n (0.9.5) 91 | concurrent-ruby (~> 1.0) 92 | jekyll (3.9.2) 93 | addressable (~> 2.4) 94 | colorator (~> 1.0) 95 | em-websocket (~> 0.5) 96 | i18n (~> 0.7) 97 | jekyll-sass-converter (~> 1.0) 98 | jekyll-watch (~> 2.0) 99 | kramdown (>= 1.17, < 3) 100 | liquid (~> 4.0) 101 | mercenary (~> 0.3.3) 102 | pathutil (~> 0.9) 103 | rouge (>= 1.7, < 4) 104 | safe_yaml (~> 1.0) 105 | jekyll-avatar (0.7.0) 106 | jekyll (>= 3.0, < 5.0) 107 | jekyll-coffeescript (1.1.1) 108 | coffee-script (~> 2.2) 109 | coffee-script-source (~> 1.11.1) 110 | jekyll-commonmark (1.4.0) 111 | commonmarker (~> 0.22) 112 | jekyll-commonmark-ghpages (0.2.0) 113 | commonmarker (~> 0.23.4) 114 | jekyll (~> 3.9.0) 115 | jekyll-commonmark (~> 1.4.0) 116 | rouge (>= 2.0, < 4.0) 117 | jekyll-default-layout (0.1.4) 118 | jekyll (~> 3.0) 119 | jekyll-feed (0.15.1) 120 | jekyll (>= 3.7, < 5.0) 121 | jekyll-gist (1.5.0) 122 | octokit (~> 4.2) 123 | jekyll-github-metadata (2.13.0) 124 | jekyll (>= 3.4, < 5.0) 125 | octokit (~> 4.0, != 4.4.0) 126 | jekyll-include-cache (0.2.1) 127 | jekyll (>= 3.7, < 5.0) 128 | jekyll-mentions (1.6.0) 129 | html-pipeline (~> 2.3) 130 | jekyll (>= 3.7, < 5.0) 131 | jekyll-optional-front-matter (0.3.2) 132 | jekyll (>= 3.0, < 5.0) 133 | jekyll-paginate (1.1.0) 134 | jekyll-readme-index (0.3.0) 135 | jekyll (>= 3.0, < 5.0) 136 | jekyll-redirect-from (0.16.0) 137 | jekyll (>= 3.3, < 5.0) 138 | jekyll-relative-links (0.6.1) 139 | jekyll (>= 3.3, < 5.0) 140 | jekyll-remote-theme (0.4.3) 141 | addressable (~> 2.0) 142 | jekyll (>= 3.5, < 5.0) 143 | jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) 144 | rubyzip (>= 1.3.0, < 3.0) 145 | jekyll-sass-converter (1.5.2) 146 | sass (~> 3.4) 147 | jekyll-seo-tag (2.8.0) 148 | jekyll (>= 3.8, < 5.0) 149 | jekyll-sitemap (1.4.0) 150 | jekyll (>= 3.7, < 5.0) 151 | jekyll-swiss (1.0.0) 152 | jekyll-theme-architect (0.2.0) 153 | jekyll (> 3.5, < 5.0) 154 | jekyll-seo-tag (~> 2.0) 155 | jekyll-theme-cayman (0.2.0) 156 | jekyll (> 3.5, < 5.0) 157 | jekyll-seo-tag (~> 2.0) 158 | jekyll-theme-dinky (0.2.0) 159 | jekyll (> 3.5, < 5.0) 160 | jekyll-seo-tag (~> 2.0) 161 | jekyll-theme-hacker (0.2.0) 162 | jekyll (> 3.5, < 5.0) 163 | jekyll-seo-tag (~> 2.0) 164 | jekyll-theme-leap-day (0.2.0) 165 | jekyll (> 3.5, < 5.0) 166 | jekyll-seo-tag (~> 2.0) 167 | jekyll-theme-merlot (0.2.0) 168 | jekyll (> 3.5, < 5.0) 169 | jekyll-seo-tag (~> 2.0) 170 | jekyll-theme-midnight (0.2.0) 171 | jekyll (> 3.5, < 5.0) 172 | jekyll-seo-tag (~> 2.0) 173 | jekyll-theme-minimal (0.2.0) 174 | jekyll (> 3.5, < 5.0) 175 | jekyll-seo-tag (~> 2.0) 176 | jekyll-theme-modernist (0.2.0) 177 | jekyll (> 3.5, < 5.0) 178 | jekyll-seo-tag (~> 2.0) 179 | jekyll-theme-primer (0.6.0) 180 | jekyll (> 3.5, < 5.0) 181 | jekyll-github-metadata (~> 2.9) 182 | jekyll-seo-tag (~> 2.0) 183 | jekyll-theme-slate (0.2.0) 184 | jekyll (> 3.5, < 5.0) 185 | jekyll-seo-tag (~> 2.0) 186 | jekyll-theme-tactile (0.2.0) 187 | jekyll (> 3.5, < 5.0) 188 | jekyll-seo-tag (~> 2.0) 189 | jekyll-theme-time-machine (0.2.0) 190 | jekyll (> 3.5, < 5.0) 191 | jekyll-seo-tag (~> 2.0) 192 | jekyll-titles-from-headings (0.5.3) 193 | jekyll (>= 3.3, < 5.0) 194 | jekyll-watch (2.2.1) 195 | listen (~> 3.0) 196 | jemoji (0.12.0) 197 | gemoji (~> 3.0) 198 | html-pipeline (~> 2.2) 199 | jekyll (>= 3.0, < 5.0) 200 | kramdown (2.3.2) 201 | rexml 202 | kramdown-parser-gfm (1.1.0) 203 | kramdown (~> 2.0) 204 | liquid (4.0.3) 205 | listen (3.7.1) 206 | rb-fsevent (~> 0.10, >= 0.10.3) 207 | rb-inotify (~> 0.9, >= 0.9.10) 208 | mercenary (0.3.6) 209 | mini_portile2 (2.8.0) 210 | minima (2.5.1) 211 | jekyll (>= 3.5, < 5.0) 212 | jekyll-feed (~> 0.9) 213 | jekyll-seo-tag (~> 2.1) 214 | minitest (5.16.3) 215 | nokogiri (1.13.9) 216 | mini_portile2 (~> 2.8.0) 217 | racc (~> 1.4) 218 | octokit (4.25.1) 219 | faraday (>= 1, < 3) 220 | sawyer (~> 0.9) 221 | pathutil (0.16.2) 222 | forwardable-extended (~> 2.6) 223 | public_suffix (4.0.7) 224 | racc (1.6.0) 225 | rb-fsevent (0.11.2) 226 | rb-inotify (0.10.1) 227 | ffi (~> 1.0) 228 | rexml (3.2.5) 229 | rouge (3.26.0) 230 | ruby2_keywords (0.0.5) 231 | rubyzip (2.3.2) 232 | safe_yaml (1.0.5) 233 | sass (3.7.4) 234 | sass-listen (~> 4.0.0) 235 | sass-listen (4.0.0) 236 | rb-fsevent (~> 0.9, >= 0.9.4) 237 | rb-inotify (~> 0.9, >= 0.9.7) 238 | sawyer (0.9.2) 239 | addressable (>= 2.3.5) 240 | faraday (>= 0.17.3, < 3) 241 | simpleidn (0.2.1) 242 | unf (~> 0.1.4) 243 | terminal-table (1.8.0) 244 | unicode-display_width (~> 1.1, >= 1.1.1) 245 | thread_safe (0.3.6) 246 | typhoeus (1.4.0) 247 | ethon (>= 0.9.0) 248 | tzinfo (1.2.10) 249 | thread_safe (~> 0.1) 250 | unf (0.1.4) 251 | unf_ext 252 | unf_ext (0.0.8.2) 253 | unicode-display_width (1.8.0) 254 | zeitwerk (2.6.1) 255 | 256 | PLATFORMS 257 | ruby 258 | 259 | DEPENDENCIES 260 | github-pages (>= 227) 261 | 262 | BUNDLED WITH 263 | 1.17.3 264 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spadille 2 | 3 | Verifiable/deterministic fair tickets generation for lotteries, raffles and gambling games. 4 | 5 | [![Azure DevOps builds](https://img.shields.io/azure-devops/build/marcoonroad/207dab13-7b85-4ce0-a62d-9af3ea14f98d/2?label=azure%20devops&logo=azure-devops)](https://dev.azure.com/marcoonroad/marcoonroad/_build?definitionId=2&_a=summary) 6 | ![Node.js CI](https://github.com/marcoonroad/spadille/workflows/Node.js%20CI/badge.svg) 7 | [![Build Status](https://travis-ci.com/marcoonroad/spadille.svg?branch=master)](https://travis-ci.com/marcoonroad/spadille) 8 | [![Coverage Status](https://coveralls.io/repos/github/marcoonroad/spadille/badge.svg?branch=master)](https://coveralls.io/github/marcoonroad/spadille?branch=master) 9 | 10 | ### About 11 | 12 | This is a library to generate random luck numbers in a fair way. Rather than relying 13 | on your process' PRNG blackbox (even if it's a cryptographic secure one), we can generate 14 | noise/randomness that is verifiable through a deterministic/replayable algorithm. Such 15 | kind of algorithms for casinos, raffles & promotions are called [Provably fair algorithms][1]. 16 | 17 | It becomes even more important when we run in a cluster of processes and we must ensure 18 | that no matter which kind of process receives the request, it must deliver the same noise 19 | output. We prove that property by using HMAC-like PRNG as shown [here][2]. We can keep 20 | the random number generation secret by a moment by just keeping the "HMAC" key secret, and 21 | then open/revealing that for clients during gambling/raffle output/outcome verification. 22 | 23 | In a broader context, it can be used for Secure Multi-party Computations too (mostly through 24 | Commitment Schemes). This library provides an API cross-compatible between servers (using Node.js 25 | engine with support to `crypto` OpenSSL's bindings) and browsers (using modern browsers supporting 26 | the `crypto.subtle` API). A good verification flow, thus, would be to generate such noise number 27 | sequences on server-side and then verifying them on client-side. 28 | 29 | ### Installation 30 | 31 | If available on NPM, just type either `npm i spadille` or `yarn add spadille`. Otherwise, 32 | you can pin this project by `npm link .` or `yarn link`, and then linking externally with 33 | either `npm link spadille` or `yarn link spadille`. The release/stable front-end CDN is 34 | available on [UNPKG][3] once the library is available on NPM beforehand. Otherwise, you can just 35 | grab the front-end minified code (at `dist/index.js`). 36 | 37 | ### Usage 38 | 39 | To generate random sequences (paired on Brazillian lotteries if you want to run a raffle without 40 | any kind of audit person): 41 | 42 | ```javascript 43 | const Lottery = spadille.lottery.brazillian; 44 | const megaSenaSequence = await Lottery.megaSena(secret, payload); 45 | const federalNumbers = await Lottery.federal(secret, payload); 46 | ``` 47 | 48 | Here, `secret` is your "HMAC-signing"-key and `payload` is a user/session-derived content (possibly 49 | a session ID, request ID, raffle counter, whatever...). The `megaSenaSequence` is a Mega-Sena lottery 50 | sequence of unique and sorted numbers between 1 and 60, inclusively. Such sequence contains 6 numbers. 51 | The `federalNumbers`, on the other hand, is a string of 5 digits, each one between 0 and 9, and this 52 | sequence may contain repeated numbers (that is, a not unique sequence). Future plans include other famous Brazillian lotteries. 53 | 54 | To generate arbitrary random sequences: 55 | 56 | ```javascript 57 | const arbitrarySequence = await spadille.prng.generate({ 58 | secret, 59 | payload, 60 | minimum: minimumInclusiveValue, 61 | maximum: maximumInclusiveValue, 62 | amount: outputSequenceLength, 63 | distinct: booleanFlag, 64 | }); 65 | ``` 66 | 67 | Such sequence can be made of many elements as you wish (but keep the eye on hardware limits, e.g, 68 | the limits of 32-bits integer representation). The number of elements are configured by the `amount` 69 | parameter. The `minimum` and `maximum` are point parameters for an inclusive interval (closed on 70 | both sides). The `distinct` is a flag to compute the sequence of unique numbers (without repetitions). 71 | 72 | If you want to generate a random number between 0 (closed interval) and 1 (open interval), there 73 | is the wrapper function `spadille.prng.rand`, inspired on the classic Random API as found in 74 | many programming languages in the wild. To use this function, just call: 75 | 76 | ```javascript 77 | const randomFraction = await spadille.prng.rand(secret, payload); 78 | ``` 79 | 80 | Given that we can generate arbitrary sequences, the random permutation algorithm becomes 81 | straightforward. This kind of permutation would just generate a random index sequence with 82 | minimum as `0`, maximum as `inputSequence.length - 1` and amount as `inputSequence.length`, 83 | where `inputSequence` is the list that we want to permute/shuffle. We then, in the end, use 84 | such random index sequence to map `inputSequence` entries into an output sequence by indexing 85 | with the random index sequence. This wrapper function is implemented as an API below: 86 | 87 | ```javascript 88 | const inputSequence = [ ... ] // an arbitrary list 89 | const outputSequence = await spadille.prng.permute({ 90 | secret, payload, inputSequence 91 | }) 92 | /* 93 | outputSequence is a random permutation of inputSequence 94 | keep in mind that there are a still unlikely probability 95 | of random collision where the inputSequence order could 96 | be preserved for outputSequence, even if this is negligible 97 | */ 98 | ``` 99 | 100 | Likewise, it's possible to take only a randomly ordered sub-sequence from the 101 | original sequence. This wrapper function is called `pick` and the contract/typing 102 | follows: 103 | 104 | ```javascript 105 | const classes = [ 106 | 'warrior', 107 | 'rogue', 108 | 'mage', 109 | 'priest', 110 | 'hunter', 111 | 'alchemist' 112 | ]; 113 | const partyClasses = await spadille.prng.pick({ 114 | secret, 115 | payload, 116 | sequence: classes, 117 | distinct: true, // optional, default is `false` for pick() 118 | amount: 3, // optional, default is `1` for pick() 119 | }); 120 | ``` 121 | 122 | Note that `{distinct: true}` only filters duplications on array-index-level, not 123 | on array-value-level, it means that if your input sequence/array contains duplicated 124 | values, they aren't deduplicated here. It also means if `{distinct: false}` and your 125 | input sequence/array contain just unique values, it is possible to generate duplicated 126 | values - it's all because picking is implemented on array-index-level generation. 127 | The default behavior of `pick` is to retrieve just one random element from a given 128 | sequence, but the output/result is still a list, thus, you will likely use the 129 | following pattern in such cases: 130 | 131 | ```javascript 132 | const [randomElement] = await spadille.prng.pick({ 133 | secret, 134 | payload, 135 | sequence 136 | }); 137 | ``` 138 | 139 | Note that `pick` will yield the same behavior of `permute` if you pass the same 140 | `secret`, `payload`, sequence for both calls, and `{distinct: true}` with 141 | `{amount: sequenceLength}` for `pick`. Therefore, `pick` is a generalisation/superset 142 | of `permute`, and the latter can contain the underlying implementation calling the 143 | former (actually this is not the case by now, but future refactor processes will end 144 | on that code deduplication). 145 | 146 | There's also a helper function provided to help you to generate fresh secrets. 147 | By using cryptograpically secure PRNGs for both Node (through `crypto` OpenSSL 148 | bindings) and browsers (through the `crypto` API), we ensure a good source of 149 | entropy for that noise string. The output string is under binary mode, but you 150 | can nevertheless convert to formats/encodings such as Base-64 and Hexadecimal. 151 | Just pass the amount of bytes to generate and be happy with that! :) 152 | 153 | ```javascript 154 | const amountOfBytes = 32; 155 | const noiseSecret = await spadille.secret.generate(amountOfBytes); 156 | ``` 157 | 158 | Remember that once you generate such secret, you should store it somewhere 159 | to retrieve later to "sign" the random sequences. And in the end, you should 160 | also publish such secret in a commitment/opening style for public verification 161 | by your users/clients. To send/receive such secret while using HTTPS requests, for 162 | instance, you can use the browser-and-Node cross-compatible Base64 encoding provided 163 | here too: 164 | 165 | ```javascript 166 | // on server-side 167 | // ... 168 | const base64Secret = spadille.base64.encode(secret); 169 | response.json({ base64Secret }); 170 | // ... 171 | 172 | // on client-side 173 | // ... 174 | const response = await axios.get(endpoint, { headers }); 175 | const secret = spadille.base64.decode(response.data.base64Secret); 176 | // the variable is ready to be used for 177 | // raffle/promotion verification on client-side here 178 | // ... 179 | ``` 180 | 181 | This helper Base64 submodule ensures that Node.js can encode a binary-content secret 182 | valid to be decoded on browsers and vice-versa. If now you are somehow confuse by this 183 | amount of functions/APIs described here, don't worry, there's a TypeScript typings 184 | file available in this library for easy IDE/Editor integration (such as 185 | auto-complete and parameters signature). 186 | 187 | 188 | ### Remarks 189 | 190 | Any doubts, enter in touch. 191 | Pull requests and issues are welcome! Have fun playing with this library! Happy hacking! 192 | 193 | [1]: https://en.wikipedia.org/wiki/Provably_fair 194 | [2]: https://cryptogambling.org/whitepapers/provably-fair-algorithms.pdf 195 | [3]: https://unpkg.com/spadille/dist/index.js 196 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # spadille 2 | 3 | Verifiable/deterministic fair tickets generation for lotteries, raffles and gambling games. 4 | 5 | [![Azure DevOps builds](https://img.shields.io/azure-devops/build/marcoonroad/207dab13-7b85-4ce0-a62d-9af3ea14f98d/2?label=azure%20devops&logo=azure-devops)](https://dev.azure.com/marcoonroad/marcoonroad/_build?definitionId=2&_a=summary) 6 | ![Node.js CI](https://github.com/marcoonroad/spadille/workflows/Node.js%20CI/badge.svg) 7 | [![Build Status](https://travis-ci.com/marcoonroad/spadille.svg?branch=master)](https://travis-ci.com/marcoonroad/spadille) 8 | [![Coverage Status](https://coveralls.io/repos/github/marcoonroad/spadille/badge.svg?branch=master)](https://coveralls.io/github/marcoonroad/spadille?branch=master) 9 | 10 | ### About 11 | 12 | This is a library to generate random luck numbers in a fair way. Rather than relying 13 | on your process' PRNG blackbox (even if it's a cryptographic secure one), we can generate 14 | noise/randomness that is verifiable through a deterministic/replayable algorithm. Such 15 | kind of algorithms for casinos, raffles & promotions are called [Provably fair algorithms][1]. 16 | 17 | It becomes even more important when we run in a cluster of processes and we must ensure 18 | that no matter which kind of process receives the request, it must deliver the same noise 19 | output. We prove that property by using HMAC-like PRNG as shown [here][2]. We can keep 20 | the random number generation secret by a moment by just keeping the "HMAC" key secret, and 21 | then open/revealing that for clients during gambling/raffle output/outcome verification. 22 | 23 | In a broader context, it can be used for Secure Multi-party Computations too (mostly through 24 | Commitment Schemes). This library provides an API cross-compatible between servers (using Node.js 25 | engine with support to `crypto` OpenSSL's bindings) and browsers (using modern browsers supporting 26 | the `crypto.subtle` API). A good verification flow, thus, would be to generate such noise number 27 | sequences on server-side and then verifying them on client-side. 28 | 29 | ### Installation 30 | 31 | If available on NPM, just type either `npm i spadille` or `yarn add spadille`. Otherwise, 32 | you can pin this project by `npm link .` or `yarn link`, and then linking externally with 33 | either `npm link spadille` or `yarn link spadille`. The release/stable front-end CDN is 34 | available on [UNPKG][3] once the library is available on NPM beforehand. Otherwise, you can just 35 | grab the front-end minified code (at `dist/index.js`). 36 | 37 | ### Usage 38 | 39 | To generate random sequences (paired on Brazillian lotteries if you want to run a raffle without 40 | any kind of audit person): 41 | 42 | ```javascript 43 | const Lottery = spadille.lottery.brazillian; 44 | const megaSenaSequence = await Lottery.megaSena(secret, payload); 45 | const federalNumbers = await Lottery.federal(secret, payload); 46 | ``` 47 | 48 | Here, `secret` is your "HMAC-signing"-key and `payload` is a user/session-derived content (possibly 49 | a session ID, request ID, raffle counter, whatever...). The `megaSenaSequence` is a Mega-Sena lottery 50 | sequence of unique and sorted numbers between 1 and 60, inclusively. Such sequence contains 6 numbers. 51 | The `federalNumbers`, on the other hand, is a string of 5 digits, each one between 0 and 9, and this 52 | sequence may contain repeated numbers (that is, a not unique sequence). Future plans include other famous Brazillian lotteries. 53 | 54 | To generate arbitrary random sequences: 55 | 56 | ```javascript 57 | const arbitrarySequence = await spadille.prng.generate({ 58 | secret, 59 | payload, 60 | minimum: minimumInclusiveValue, 61 | maximum: maximumInclusiveValue, 62 | amount: outputSequenceLength, 63 | distinct: booleanFlag, 64 | }); 65 | ``` 66 | 67 | Such sequence can be made of many elements as you wish (but keep the eye on hardware limits, e.g, 68 | the limits of 32-bits integer representation). The number of elements are configured by the `amount` 69 | parameter. The `minimum` and `maximum` are point parameters for an inclusive interval (closed on 70 | both sides). The `distinct` is a flag to compute the sequence of unique numbers (without repetitions). 71 | 72 | If you want to generate a random number between 0 (closed interval) and 1 (open interval), there 73 | is the wrapper function `spadille.prng.rand`, inspired on the classic Random API as found in 74 | many programming languages in the wild. To use this function, just call: 75 | 76 | ```javascript 77 | const randomFraction = await spadille.prng.rand(secret, payload); 78 | ``` 79 | 80 | Given that we can generate arbitrary sequences, the random permutation algorithm becomes 81 | straightforward. This kind of permutation would just generate a random index sequence with 82 | minimum as `0`, maximum as `inputSequence.length - 1` and amount as `inputSequence.length`, 83 | where `inputSequence` is the list that we want to permute/shuffle. We then, in the end, use 84 | such random index sequence to map `inputSequence` entries into an output sequence by indexing 85 | with the random index sequence. This wrapper function is implemented as an API below: 86 | 87 | ```javascript 88 | const inputSequence = [ ... ] // an arbitrary list 89 | const outputSequence = await spadille.prng.permute({ 90 | secret, payload, inputSequence 91 | }) 92 | /* 93 | outputSequence is a random permutation of inputSequence 94 | keep in mind that there are a still unlikely probability 95 | of random collision where the inputSequence order could 96 | be preserved for outputSequence, even if this is negligible 97 | */ 98 | ``` 99 | 100 | Likewise, it's possible to take only a randomly ordered sub-sequence from the 101 | original sequence. This wrapper function is called `pick` and the contract/typing 102 | follows: 103 | 104 | ```javascript 105 | const classes = [ 106 | 'warrior', 107 | 'rogue', 108 | 'mage', 109 | 'priest', 110 | 'hunter', 111 | 'alchemist' 112 | ]; 113 | const partyClasses = await spadille.prng.pick({ 114 | secret, 115 | payload, 116 | sequence: classes, 117 | distinct: true, // optional, default is `false` for pick() 118 | amount: 3, // optional, default is `1` for pick() 119 | }); 120 | ``` 121 | 122 | Note that `{distinct: true}` only filters duplications on array-index-level, not 123 | on array-value-level, it means that if your input sequence/array contains duplicated 124 | values, they aren't deduplicated here. It also means if `{distinct: false}` and your 125 | input sequence/array contain just unique values, it is possible to generate duplicated 126 | values - it's all because picking is implemented on array-index-level generation. 127 | The default behavior of `pick` is to retrieve just one random element from a given 128 | sequence, but the output/result is still a list, thus, you will likely use the 129 | following pattern in such cases: 130 | 131 | ```javascript 132 | const [randomElement] = await spadille.prng.pick({ 133 | secret, 134 | payload, 135 | sequence 136 | }); 137 | ``` 138 | 139 | Note that `pick` will yield the same behavior of `permute` if you pass the same 140 | `secret`, `payload`, sequence for both calls, and `{distinct: true}` with 141 | `{amount: sequenceLength}` for `pick`. Therefore, `pick` is a generalisation/superset 142 | of `permute`, and the latter can contain the underlying implementation calling the 143 | former (actually this is not the case by now, but future refactor processes will end 144 | on that code deduplication). 145 | 146 | There's also a helper function provided to help you to generate fresh secrets. 147 | By using cryptograpically secure PRNGs for both Node (through `crypto` OpenSSL 148 | bindings) and browsers (through the `crypto` API), we ensure a good source of 149 | entropy for that noise string. The output string is under binary mode, but you 150 | can nevertheless convert to formats/encodings such as Base-64 and Hexadecimal. 151 | Just pass the amount of bytes to generate and be happy with that! :) 152 | 153 | ```javascript 154 | const amountOfBytes = 32; 155 | const noiseSecret = await spadille.secret.generate(amountOfBytes); 156 | ``` 157 | 158 | Remember that once you generate such secret, you should store it somewhere 159 | to retrieve later to "sign" the random sequences. And in the end, you should 160 | also publish such secret in a commitment/opening style for public verification 161 | by your users/clients. To send/receive such secret while using HTTPS requests, for 162 | instance, you can use the browser-and-Node cross-compatible Base64 encoding provided 163 | here too: 164 | 165 | ```javascript 166 | // on server-side 167 | // ... 168 | const base64Secret = spadille.base64.encode(secret); 169 | response.json({ base64Secret }); 170 | // ... 171 | 172 | // on client-side 173 | // ... 174 | const response = await axios.get(endpoint, { headers }); 175 | const secret = spadille.base64.decode(response.data.base64Secret); 176 | // the variable is ready to be used for 177 | // raffle/promotion verification on client-side here 178 | // ... 179 | ``` 180 | 181 | This helper Base64 submodule ensures that Node.js can encode a binary-content secret 182 | valid to be decoded on browsers and vice-versa. If now you are somehow confuse by this 183 | amount of functions/APIs described here, don't worry, there's a TypeScript typings 184 | file available in this library for easy IDE/Editor integration (such as 185 | auto-complete and parameters signature). 186 | 187 | 188 | ### Remarks 189 | 190 | Any doubts, enter in touch. 191 | Pull requests and issues are welcome! Have fun playing with this library! Happy hacking! 192 | 193 | [1]: https://en.wikipedia.org/wiki/Provably_fair 194 | [2]: https://cryptogambling.org/whitepapers/provably-fair-algorithms.pdf 195 | [3]: https://unpkg.com/spadille/dist/index.js 196 | --------------------------------------------------------------------------------