<file_sep># @provair/seed

> The provair client seed and server seed generator
<file_sep>import prog from 'caporal';
import {assert} from 'ts-essentials';
import {PassThrough} from 'readable-stream';
import fs from 'fs';
import {Randomizer} from '../randomizer';

export const DEFAULT_WIDTH = 64;

export const DEFAULT_OPTIONS = {
  blocks: 1,
  width: DEFAULT_WIDTH,
};

export function setup() {
  prog
    .command('rand', 'Generate random bytes using provair algorithms')
    .option('-o, --out <file>', 'Output file')
    .option('-b, --binary', 'Output in raw binary format')
    .option(
      '-k, --blocks <1K_Blocks>',
      'Size of output in kilobytes',
      prog.INT,
      1,
    )
    .option(
      '-w, --width [1...256]',
      'Byte per line of output',
      prog.INT,
      DEFAULT_WIDTH,
    )
    .action(handleRandom);
}

export function handleRandom(
  args: Record<string, any>,
  options: Record<string, any>,
  logger: Logger,
) {
  const {out, binary, blocks, width} = Object.assign(
    {},
    DEFAULT_OPTIONS,
    options,
  );

  assert(
    width >= 1 && width <= 256,
    `width should between 1 and 255, but got ${width}`,
  );
  const randomizer = new Randomizer({size: blocks * 1024});
  const normalizer = new Normalizer({hex: binary ? false : width});
  const output = out ? fs.createWriteStream(out) : process.stdout;
  randomizer.pipe(normalizer).pipe(output);
}

export interface NormalizerOptions {
  hex: number | boolean;
}

export class Normalizer extends PassThrough {
  protected binary: boolean;
  protected width: number;

  private cache: Buffer;
  private count: number;

  constructor(options: NormalizerOptions) {
    super();
    this.count = 0;
    this.cache = Buffer.alloc(0);
    this.binary = !options.hex;
    if (options.hex) {
      this.width =
        typeof options.hex === 'number' ? options.hex : DEFAULT_WIDTH;
    }
  }

  _transform<T>(
    chunk: T,
    encoding: BufferEncoding | string | null | undefined,
    callback: (error?: Error, data?: T) => void,
  ) {
    if (this.binary || !Buffer.isBuffer(chunk)) {
      return super._transform(chunk, encoding, callback);
    }

    const bytes = this.width / 2;

    let data = Buffer.concat([this.cache, chunk]);
    while (data.length > bytes) {
      this.push(data.slice(0, bytes).toString('hex') + '\n');
      data = data.slice(bytes);
    }
    this.cache = data;
    callback();
  }
}
<file_sep>import {inject} from '@loopback/core';

import {param, post, Request, requestBody, RestBindings} from '@loopback/rest';
import {PROVABLY_SERVICE} from '../keys';
import {ProvablyService} from '../services';
import {ProvableServerSeedResult, ProvablyCalculateParams} from '@provair/core';

export class ProvablyController {
  constructor(
    @inject(PROVABLY_SERVICE) private provablyService: ProvablyService,
    @inject(RestBindings.Http.REQUEST) private request: Request,
  ) {}

  @post('/seed/{uid}', {
    responses: {
      200: {
        content: {
          'application/json': {
            schema: {
              type: 'object',
              properties: {
                uid: 'string',
                serverSeedHash: 'string',
                expires: {
                  type: 'string',
                  format: 'date-time',
                },
              },
            },
          },
        },
        description: 'Server seed hash',
      },
    },
  })
  async createServerSeed(
    @param.path.string('uid') uid: string,
  ): Promise<ProvableServerSeedResult> {
    return this.provablyService.creatServerSeed({uid});
  }

  @post('/calc/{uid}', {
    responses: {
      200: {
        content: {
          'application/json': {
            schema: {
              type: 'array',
              items: {
                type: 'object',
                properties: {
                  result: 'number',
                  serverSeed: 'string',
                  clientSeed: 'string',
                  nonce: 'number',
                },
              },
            },
          },
        },
        description: 'The calculated result',
      },
    },
  })
  async calculate(
    @param.path.string('uid') uid: string,
    @requestBody({
      content: {
        'application/json': {
          schema: {
            type: 'object',
            properties: {
              serverSeedHash: {
                type: 'string',
              },
              clientSeed: {
                type: 'string',
              },
              count: {
                type: 'number',
              },
            },
            required: ['serverSeedHash', 'clientSeed'],
          },
        },
      },
    })
    params: ProvablyCalculateParams,
  ) {
    return this.provablyService.calculate({uid, ...params});
  }

  @post('/verify', {
    responses: {
      200: {
        content: {
          'application/json': {
            schema: {
              type: 'object',
              properties: {
                serverSeedHash: 'string',
                result: 'number',
              },
            },
          },
        },
        description: 'The calculated result',
      },
    },
  })
  async verify(
    @requestBody({
      content: {
        'application/json': {
          schema: {
            type: 'object',
            properties: {
              serverSeed: {
                type: 'string',
              },
              clientSeed: {
                type: 'string',
              },
              nonce: {
                type: 'number',
              },
            },
          },
        },
      },
    })
    {
      serverSeed,
      clientSeed,
      nonce,
    }: {
      serverSeed: string;
      clientSeed: string;
      nonce?: number;
    },
  ) {
    return this.provablyService.verify(serverSeed, clientSeed, nonce);
  }
}
<file_sep>import {Strategy} from '../../types';

export class MockStrategy implements Strategy {
  calc(hex: Buffer | string): number {
    return 0;
  }
}
<file_sep>import {bind, BindingScope, ContextTags, inject} from '@loopback/core';
import {
  CalculateOptions,
  Provably,
  ProvablyCalculateParams,
  ProvablyServerSeedParams,
} from '@provair/core';
import {Store} from 'kvs';
import {PROVABLY_SERVICE} from '../keys';

const defaultConfig = {
  store: {
    name: 'memory',
  },
};

@bind({
  scope: BindingScope.SINGLETON,
  tags: {[ContextTags.KEY]: PROVABLY_SERVICE},
})
export class ProvablyService {
  protected provably: Provably;

  constructor(
    @inject('provably', {optional: true}) config: any = defaultConfig,
  ) {
    this.provably = Provably.create(Store.create(config.store));
  }

  async creatServerSeed(params: ProvablyServerSeedParams) {
    return this.provably.createServerSeed(params);
  }

  async calculate(params: ProvablyCalculateParams) {
    return this.provably.calculate(params);
  }

  async verify(
    serverSeed: string,
    clientSeed: string,
    nonce?: number,
    options?: CalculateOptions,
  ) {
    return this.provably.verify(serverSeed, clientSeed, nonce, options);
  }
}
<file_sep>import {HashCtor} from './types';

const SHA256 = <HashCtor>require('bcrypto/lib/sha256');
const SHA512 = <HashCtor>require('bcrypto/lib/sha512');

export {SHA256, SHA512};

export const algs: {
  [name: string]: HashCtor;
} = {
  sha256: SHA256,
  sha512: SHA512,
};

export function getHash(nameOrHash: string | HashCtor): HashCtor {
  if (isHashCtor(nameOrHash)) {
    return nameOrHash;
  }
  const Hash = algs[nameOrHash.toLowerCase()];
  if (!Hash) {
    throw new Error(`Unsupported hash algorithm: ${nameOrHash}`);
  }
  return Hash;
}

export function isHashCtor(x: any): x is HashCtor {
  return typeof x === 'function' && typeof x.digest === 'function';
}
<file_sep>import {expect} from '@loopback/testlab';
import {genClientSeed} from '../../../seed';
import {generateServerSeed} from '../seeds';
import {calculate} from '../calculator';

describe('calculator', () => {
  it('should calc', function () {
    const {seed} = generateServerSeed();
    const clientSeed = genClientSeed();
    const {nonce, result} = calculate(seed, clientSeed);
    expect(nonce).equal(0);
    expect(result).greaterThanOrEqual(0);
    expect(result).lessThanOrEqual(100);
  });
});
<file_sep>#!/usr/bin/env node

require('..').cli(process.argv).catch(err => {
  console.error(err);
  process.exit(1);
});
<file_sep>import {expect} from '@loopback/testlab';
import {getHash, SHA256, SHA512} from '../algs';

describe('algs', function () {
  it('should have correct hash algorithms', function () {
    expect(getHash('sha256')).equal(SHA256);
    expect(getHash('SHA256')).equal(SHA256);
    expect(getHash(SHA256)).equal(SHA256);
    expect(getHash('sha512')).equal(SHA512);
    expect(getHash('SHA512')).equal(SHA512);
    expect(getHash(SHA512)).equal(SHA512);

    expect(() => getHash('MD5')).throw(/Unsupported hash algorithm/);
  });
});
<file_sep># Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# 0.5.0 (2020-09-13)


### Features

* initial commit ([bcbc3cc](https://github.com/provair/provair/commit/bcbc3cc923ef1af75ac89027a3e75f101f7b18d1))





# 0.4.0 (2020-09-13)


### Features

* initial commit ([c357241](https://gitr.net/provair/provair/commits/c357241a2ab2c33d8689329fa8857ab1a09d3c9d))





## [0.3.3](https://github.com-unknomn/provair/provair/compare/@provair/core@0.3.2...@provair/core@0.3.3) (2020-09-13)

**Note:** Version bump only for package @provair/core





## [0.3.2](https://github.com-unknomn/provair/provair/compare/@provair/core@0.3.1...@provair/core@0.3.2) (2020-09-13)

**Note:** Version bump only for package @provair/core





## [0.3.1](https://github.com-unknomn/provair/provair/compare/@provair/core@0.3.0...@provair/core@0.3.1) (2020-09-10)


### Bug Fixes

* verify default nonce to 0 ([687d5cb](https://github.com-unknomn/provair/provair/commit/687d5cbe0c8e628b02befeb5afa2326bdf97f397))





# [0.3.0](https://github.com-unknomn/provair/provair/compare/@provair/core@0.2.3...@provair/core@0.3.0) (2020-09-10)


### Features

* add revokeServerSeed support and allow multiple values to be calculated at once ([bff443b](https://github.com-unknomn/provair/provair/commit/bff443bc20e39fbafa54e99d2a5e5008afb8ebaa))





## [0.2.3](https://github.com-unknomn/provair/provair/compare/@provair/core@0.2.2...@provair/core@0.2.3) (2020-09-10)

**Note:** Version bump only for package @provair/core





## [0.2.2](https://github.com-unknomn/provair/provair/compare/@provair/core@0.2.1...@provair/core@0.2.2) (2020-09-10)


### Bug Fixes

* import genClientSeed from @provair/seed ([6372ed4](https://github.com-unknomn/provair/provair/commit/6372ed42b95513ecbaa8c2ad8daa12c05b9a1852))





## [0.2.1](https://github.com-unknomn/provair/provair/compare/@provair/core@0.2.0...@provair/core@0.2.1) (2020-09-08)

**Note:** Version bump only for package @provair/core





# 0.2.0 (2020-09-08)


### Features

* initial commit ([89e325a](https://github.com-unknomn/provair/provair/commit/89e325aad139413e930658b73fa6b24d8ac22e66))
<file_sep># @provair/core

> The core library of provably fair algorithm
<file_sep>import {
  Client,
  createRestAppClient,
  expect,
  givenHttpServerConfig,
} from '@loopback/testlab';
import {ProvablyApplication} from '../..';

const userId = '123';

describe('ProvablyApplication', () => {
  let app: ProvablyApplication;
  let client: Client;

  before(givenRunningApplicationWithCustomConfiguration);
  after(() => app.stop());

  before(() => {
    client = createRestAppClient(app);
  });

  it('create a server seed', async function () {
    const {body} = await client.post(`/seed/${userId}`).expect(200);

    expect(body.uid).equal(userId);
    expect(body.serverSeedHash).type('string');
    expect(body.serverSeedHash).lengthOf(64); // 32 byte
    expect(body.expires).type('string');
    expect(new Date(body.expires)).instanceOf(Date);
  });

  it('calculate provably number', async function () {
    const {
      body: {serverSeedHash},
    } = await client.post(`/seed/${userId}`).expect(200);
    const {body} = await client
      .post(`/calc/${userId}`)
      .send({serverSeedHash, clientSeed: 'hello'})
      .expect(200);

    expect(Array.isArray(body)).true();
    expect(body).lengthOf(1);

    const [answer] = body;
    expect(answer.result).type('number');
    expect(answer.serverSeed).type('string');
    expect(answer.clientSeed).type('string');
    expect(answer.nonce).type('number');
  });

  it('verify result', async function () {
    const {body} = await client
      .post('/verify')
      .send({
        serverSeed: 'fe<KEY>',
        clientSeed: 'hello',
      })
      .expect(200);
    expect(body.serverSeedHash).type('string');
    expect(body.serverSeedHash).lengthOf(64);
    expect(body.result).type('number');
  });

  async function givenRunningApplicationWithCustomConfiguration() {
    app = new ProvablyApplication({
      rest: givenHttpServerConfig(),
    });

    // Start Application
    await app.main();
  }
});
<file_sep>import prog from 'caporal';

import {rand} from './cmds';

const pkg = require('../package.json');

export async function cli(argv: any[]) {
  prog.version(pkg.version).description('The provair command line tools');

  rand.setup();

  prog.parse(argv);
}

if (require.main === module) {
  cli(process.argv).catch(err => {
    console.error(err);
    process.exit(1);
  });
}
<file_sep># Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# 0.4.0 (2020-09-13)


### Features

* initial commit ([bcbc3cc](https://github.com/provair/provair/commit/bcbc3cc923ef1af75ac89027a3e75f101f7b18d1))





# 0.3.0 (2020-09-13)


### Features

* initial commit ([c357241](https://gitr.net/provair/provair/commits/c357241a2ab2c33d8689329fa8857ab1a09d3c9d))





## [0.2.4](https://github.com-unknomn/provair/provair/compare/@provair/seed@0.2.3...@provair/seed@0.2.4) (2020-09-13)

**Note:** Version bump only for package @provair/seed





## [0.2.3](https://github.com-unknomn/provair/provair/compare/@provair/seed@0.2.2...@provair/seed@0.2.3) (2020-09-13)

**Note:** Version bump only for package @provair/seed





## [0.2.2](https://github.com-unknomn/provair/provair/compare/@provair/seed@0.2.1...@provair/seed@0.2.2) (2020-09-10)

**Note:** Version bump only for package @provair/seed





## [0.2.1](https://github.com-unknomn/provair/provair/compare/@provair/seed@0.2.0...@provair/seed@0.2.1) (2020-09-08)

**Note:** Version bump only for package @provair/seed





# 0.2.0 (2020-09-08)


### Features

* initial commit ([89e325a](https://github.com-unknomn/provair/provair/commit/89e325aad139413e930658b73fa6b24d8ac22e66))
<file_sep>import {expect} from '@loopback/testlab';
import {simulator} from '../simulator';

describe('simulator', function () {
  it('should simulate number', function () {
    const num = simulator.number();
    expect(num).type('number');
    expect(num).lessThanOrEqual(100);
    expect(num).greaterThanOrEqual(0);
  });

  it('should simulate random', function () {
    const data = simulator.random();
    expect(Buffer.isBuffer(data)).ok();
    expect(data).lengthOf(64);
  });
});
<file_sep>import {HashCtor, Strategy, StrategyCtor} from './types';
import {SHA512} from './algs';
import {resolveStrategy} from './strategies';

export interface CalculateOptions {
  alg?: string | HashCtor;
  strategy?: Strategy | StrategyCtor | string;
}

export interface CalculateResult {
  result: number;
  nonce: number;
  serverSeed: string;
  clientSeed: string;
}

const DEFAULT_CALCULATE_OPTIONS = {
  alg: 'sha512',
  strategy: 'classic',
};

export function aggregate(serverSeed: string, clientSeed: string, nonce = 0) {
  const ss = Buffer.from(serverSeed, 'hex');
  // client seed is generic string
  const cs = Buffer.from(`${clientSeed}-${Math.abs(nonce)}`);
  return SHA512.hmac().init(ss).update(cs).final();
}

export function calculate(
  serverSeed: string,
  clientSeed: string,
  nonce = 0,
  options: CalculateOptions = {},
): CalculateResult {
  const opts = Object.assign({}, DEFAULT_CALCULATE_OPTIONS, options);
  const strategy = resolveStrategy(opts.strategy);
  const seed = aggregate(serverSeed, clientSeed, nonce);
  const result = strategy.calc(seed);
  return {result, serverSeed, clientSeed, nonce};
}
<file_sep>{
  "extends": "@loopback/build/config/tsconfig.common.json",
  "compilerOptions": {
    "composite": true
  },
  "references": [
    {
      "path": "examples/server/tsconfig.json"
    },
    {
      "path": "packages/cli/tsconfig.json"
    },
    {
      "path": "packages/core/tsconfig.json"
    },
    {
      "path": "packages/seed/tsconfig.json"
    }
  ],
  "files": []
}
<file_sep>import {Readable, ReadableOptions} from 'readable-stream';
import {simulator} from '@provair/core';

export type RandomizerOptions = ReadableOptions & {
  size?: number;
};

export class Randomizer extends Readable {
  protected readCount: number;
  protected size: number;

  protected buf: Buffer;

  constructor(options?: RandomizerOptions) {
    super(options);
    this.readCount = 0;
    this.size = options?.size ?? 1024;
  }

  _read(size: number) {
    let shouldEnd = false;
    if (size >= this.size - this.readCount) {
      size = this.size - this.readCount;
      shouldEnd = true;
    }

    if (size > 0) {
      while (!this.buf || this.buf.length < size) {
        const generated = simulator.random();
        this.buf = this.buf ? Buffer.concat([this.buf, generated]) : generated;
      }
      this.push(this.buf.slice(0, size));
      this.buf = this.buf.slice(size);
      this.readCount += size;
    }

    if (shouldEnd) {
      this.push(null);
    }
  }
}
<file_sep>import {expect} from '@loopback/testlab';
import {ClassicStrategy} from '../../strategies/classic';

const classic = new ClassicStrategy();

describe('strategy/classic', function () {
  it('should calculate number with fist 5 characters', function () {
    const hex = 'F000112345';
    const answer = classic.calc(hex);
    expect(answer).equal(parseInt('F0001', 16) / 10000);
  });

  it('should calculate number with second 5 characters', function () {
    const hex = 'FFFFF12345';
    const answer = classic.calc(hex);
    expect(answer).equal(parseInt('12345', 16) / 10000);
  });

  it('should calculate number with last characters', function () {
    const hex = 'FFFFF1234';
    const answer = classic.calc(hex);
    expect(answer).equal(parseInt('1234', 16) / 10000);
  });
});
<file_sep>import {expect} from '@loopback/testlab';
import {
  DEFAULT_CLIENT_SEED_BYTES,
  DEFAULT_RANDOM_HEX_BYTES,
  DEFAULT_SERVER_SEED_BYTES,
  genClientSeed,
  genServerSeed,
  randomString,
  randomHex,
  AlphabetAndNumberCharacters,
} from '..';

describe('seeds', () => {
  it('should generate ascii', function () {
    const seed = randomString(1024);
    expect(seed).length(1024);
    for (const c of seed) {
      expect(/[0-9a-zA-Z]/.test(c)).true();
    }
  });

  it('should generate correct hex length with default length', function () {
    expect(randomHex()).lengthOf(DEFAULT_RANDOM_HEX_BYTES * 2);
  });

  it('should generate client seed', function () {
    const seed = genClientSeed();
    expect(seed).length(DEFAULT_CLIENT_SEED_BYTES);
    for (const c of seed) {
      expect(AlphabetAndNumberCharacters.includes(c)).true();
    }
  });

  it('should generate server seed', function () {
    const seed = genServerSeed();
    expect(seed).length(DEFAULT_SERVER_SEED_BYTES * 2);
    for (const c of seed) {
      expect(/[0-9A-Fa-f]/.test(c)).true();
    }
  });
});
<file_sep>export * from './provably.controller';
<file_sep>import {Strategy} from '../types';

export class ClassicStrategy implements Strategy {
  readonly name = 'classic';

  calc(data: Buffer | string): number {
    const hex = Buffer.isBuffer(data) ? data.toString('hex') : data;

    let answer = Infinity;
    let i = 0;

    while (i < hex.length && answer > 999999) {
      answer = parseInt(hex.slice(i, i + 5).toString(), 16);
      i = Math.min(i + 5, hex.length);
    }
    return answer / 10000;
  }
}

export default ClassicStrategy;
<file_sep>import {expect} from '@loopback/testlab';
import {
  DEFAULT_RANDOM_HEX_BYTES,
  DEFAULT_RANDOM_HEX_STRING_BYTES,
  randomHex,
  randomHexStr,
} from '../utils';

describe('utils', function () {
  it('should random hex', function () {
    const hex = randomHex(21);
    expect(hex).lengthOf(21);
  });

  it('should random hex string', function () {
    const hex = randomHexStr(12);
    expect(hex).lengthOf(24);
  });

  it('should random hex with default length', function () {
    const hex = randomHex();
    expect(hex).lengthOf(DEFAULT_RANDOM_HEX_BYTES);
  });

  it('should random hex string with default length', function () {
    const hex = randomHexStr();
    expect(hex).lengthOf(DEFAULT_RANDOM_HEX_STRING_BYTES * 2);
  });
});
<file_sep>export * from './types';
export * from './algs';
export * from './seeds';
export * from './calculator';
export * from './simulator';
export * from './provably';
<file_sep>import {expect} from '@loopback/testlab';
import {randomHex} from '../utils';
import {generateServerSeed} from '../seeds';

describe('seeds', () => {
  describe('server seed', function () {
    it('should generate server seed with length', () => {
      const {seed, hash} = generateServerSeed(12);
      expect(seed).type('string');
      expect(hash).type('string');
      expect(seed).lengthOf(24);
      expect(hash).lengthOf(64);
    });

    it('should generate server seed with default', function () {
      const {seed, hash} = generateServerSeed();
      expect(seed).type('string');
      expect(hash).type('string');
      expect(seed).lengthOf(32);
      expect(hash).lengthOf(64);
    });

    it('should generate server seed with an existing seed', function () {
      const {seed, hash} = generateServerSeed(randomHex(12));
      expect(seed).type('string');
      expect(hash).type('string');
      expect(seed).lengthOf(24);
      expect(hash).lengthOf(64);
    });
  });
});
<file_sep>import {Bucket, Store, StoreOptions} from 'kvs';
import {generateServerSeed} from './seeds';
import {calculate, CalculateOptions, CalculateResult} from './calculator';

export const genkey = (hash: Buffer | string) =>
  `provair:seed:${Buffer.isBuffer(hash) ? hash.toString('hex') : hash}`;

export interface ProvableServerSeed {
  uid: string;
  seed: string;
}

export interface ProvableServerSeedResult {
  uid: string;
  serverSeedHash: string;
  expires: Date;
}

export interface ProvablyServerSeedParams {
  uid?: string;
  expiresIn?: number;
}

export interface ProvablyCalculateParams extends CalculateOptions {
  uid?: string;
  serverSeedHash: string;
  clientSeed: string;
  count?: number;
}

export interface ProvableServerSeedQuery {
  uid?: string;
  serverSeedHash: string;
}

/**
 * The main provair class to generate server hash and calculate the random number
 */
export class Provably {
  readonly bucket: Promise<Bucket>;

  static create(name: string, options?: StoreOptions): Provably;
  static create(store: Store): Provably;
  static create(store: Store | string, options?: StoreOptions) {
    if (typeof store === 'string') {
      store = Store.create(store, options);
    }
    return new Provably(store);
  }

  constructor(readonly store: Store) {
    this.bucket = store.bucket('provair:seed');
  }

  /**
   * Create server seed and save it to kvs store.
   *
   * @param params
   */
  async createServerSeed(
    params: ProvablyServerSeedParams = {},
  ): Promise<ProvableServerSeedResult> {
    const {uid = 'none', expiresIn = 60 * 60} = params;
    const expires = new Date(Date.now() + expiresIn * 1000);
    const {seed, hash} = generateServerSeed();
    const bucket = await this.bucket;
    await bucket.set(genkey(hash), <ProvableServerSeed>{uid, seed}, expiresIn);
    return {uid, serverSeedHash: hash, expires};
  }

  async revokeServerSeed(query: ProvableServerSeedQuery): Promise<boolean> {
    const {uid = 'none', serverSeedHash} = query;
    const bucket = await this.bucket;
    const key = genkey(serverSeedHash);
    const data: ProvableServerSeed = await bucket.get(key);
    if (data?.uid === uid) {
      await bucket.del(key);
      return true;
    }
    return false;
  }

  async exists(query: ProvableServerSeedQuery): Promise<boolean> {
    const {uid = 'none', serverSeedHash} = query;
    const bucket = await this.bucket;
    const key = genkey(serverSeedHash);
    const data: ProvableServerSeed = await bucket.get(key);
    return data?.uid === uid;
  }

  /**
   * Calculator provably number with server seed and client seed
   *
   * @param params
   */
  async calculate(params: ProvablyCalculateParams): Promise<CalculateResult[]> {
    const {
      uid = 'none',
      serverSeedHash,
      clientSeed,
      count = 1,
      ...options
    } = params;
    const bucket = await this.bucket;
    const key = genkey(serverSeedHash);
    const data: ProvableServerSeed = await bucket.get(key);
    if (!data) {
      throw new Error('Invalid serverSeedHash, probably already used');
    }
    if (data.uid !== uid) {
      throw new Error('Provided uid does not match token serverSeed.uid');
    }
    await bucket.del(key);

    const results: CalculateResult[] = [];
    for (let nonce = 0; nonce < count; nonce++) {
      results.push(calculate(data.seed, clientSeed, nonce, options));
    }
    return results;
  }

  verify(
    serverSeed: string,
    clientSeed: string,
    nonce = 0,
    options?: CalculateOptions,
  ) {
    const {hash} = generateServerSeed(serverSeed);
    const answer = calculate(serverSeed, clientSeed, nonce, options);
    return {result: answer.result, serverSeedHash: hash};
  }
}
<file_sep>import {Store} from 'kvs';
import {expect} from '@loopback/testlab';
import {genkey, Provably} from '../provably';
import {Strategy} from '../types';

const clientSeed = 'clientSeed';
const customStrategy: Strategy = {
  calc(hex: Buffer | string): number {
    return 0.5;
  },
};

describe('provably', function () {
  describe('initiation', function () {
    it('should initiate with store name', function () {
      const provably = Provably.create('memory');
      expect(provably).ok();
    });

    it('should initiate with store instance', function () {
      const provably = Provably.create(Store.create('memory'));
      expect(provably).ok();
    });
  });

  describe('genkey', function () {
    it('should genkey with string key', function () {
      expect(genkey('123')).equal('provair:seed:123');
    });
    it('should genkey with buffer key', function () {
      expect(genkey(Buffer.from('123'))).equal('provair:seed:313233');
    });
  });

  describe('functions', function () {
    const store = Store.create('memory');
    const provably = Provably.create(store);

    afterEach(async () => {
      await (await provably.bucket).clear();
    });

    describe('create server seed', function () {
      it('should create server seed hash', async function () {
        const ts = Date.now();
        const result = await provably.createServerSeed();
        expect(result).ok();
        expect(result.uid).equal('none');
        expect(result.serverSeedHash).lengthOf(64);
        expect(result.expires).greaterThanOrEqual(ts + 3600000);
        expect(result.expires).belowOrEqual(Date.now() + 3600000);
      });
    });

    describe('check server seed exists', function () {
      it('should return true if server seed exists', async function () {
        const result = await provably.createServerSeed();
        expect(await provably.exists(result)).true();
      });

      it('should return false if server seed not exists', async function () {
        expect(await provably.exists({serverSeedHash: 'unknown'})).false();
      });

      it('should return false if uid not match', async function () {
        const result = await provably.createServerSeed();
        expect(
          await provably.exists({
            uid: 'unknown',
            serverSeedHash: result.serverSeedHash,
          }),
        ).false();
      });
    });

    describe('revoke server seed', function () {
      it('should return that revoked the server seed that exists', async function () {
        const ss = await provably.createServerSeed();
        expect(await provably.exists(ss)).true();
        const result = await provably.revokeServerSeed(ss);
        expect(result).true();
        expect(await provably.exists(ss)).false();
      });

      it('should return false when trying to revoke the server seed that not exists', async function () {
        const result = await provably.revokeServerSeed({
          serverSeedHash: 'unknown',
        });
        expect(result).false();
      });
    });

    describe('calculate', function () {
      let serverSeedHash: string;
      beforeEach(async () => {
        const result = await provably.createServerSeed();
        serverSeedHash = result.serverSeedHash;
      });
      it('should fail if wrong id', async function () {
        await expect(
          provably.calculate({
            uid: 'other',
            serverSeedHash,
            clientSeed,
            strategy: customStrategy,
          }),
        ).rejectedWith(/Provided uid does not match/);
      });

      it('should fail if redis error', async function () {
        await expect(
          provably.calculate({
            serverSeedHash: 'unknown',
            clientSeed,
            strategy: customStrategy,
          }),
        ).rejectedWith(/Invalid serverSeedHash/);
      });

      it('should fail if same hash', async function () {
        await provably.calculate({
          serverSeedHash,
          clientSeed,
          strategy: customStrategy,
        });
        await expect(
          provably.calculate({
            serverSeedHash,
            clientSeed,
            strategy: customStrategy,
          }),
        ).rejectedWith(/Invalid serverSeedHash/);
      });

      it('should succeed to calculate one result', async function () {
        const [answer] = await provably.calculate({
          serverSeedHash,
          clientSeed,
          strategy: customStrategy,
        });
        expect(answer.result).equal(0.5);
      });

      it('should succeed to calculate multiple results', async function () {
        const answers = await provably.calculate({
          serverSeedHash,
          clientSeed,
          count: 5,
        });
        expect(answers).lengthOf(5);
        for (let i = 0; i < answers.length; i++) {
          expect(answers[i].result).type('number');
          expect(answers[i].nonce).equal(i);
        }
      });
    });

    describe('verifying', function () {
      it('should verify with default nonce', async function () {
        const {serverSeedHash} = await provably.createServerSeed();
        const [calculated] = await provably.calculate({
          serverSeedHash,
          clientSeed,
        });
        const verified = provably.verify(
          calculated.serverSeed,
          calculated.clientSeed,
        );
        expect(calculated.result).equal(verified.result);
      });

      it('should verify multiple results', async function () {
        const {serverSeedHash} = await provably.createServerSeed();
        const answers = await provably.calculate({
          serverSeedHash,
          clientSeed,
          count: 5,
        });

        for (const answer of answers) {
          const verified = provably.verify(
            answer.serverSeed,
            answer.clientSeed,
            answer.nonce,
          );
          expect(answer.result).equal(verified.result);
        }
      });
    });
  });
});
<file_sep>import {genClientSeed} from '@provair/seed';
import {generateServerSeed} from './seeds';
import {aggregate, calculate, CalculateOptions} from './calculator';

/**
 * Simulate provair randoms such as hash or number values.
 *
 */
export class Simulator {
  number(options?: CalculateOptions) {
    const {seed} = generateServerSeed();
    const clientSeed = genClientSeed();
    const {result} = calculate(seed, clientSeed, 0, options);
    return result;
  }

  random() {
    const {seed} = generateServerSeed();
    const clientSeed = genClientSeed();
    return aggregate(seed, clientSeed);
  }
}

export const simulator = new Simulator();
<file_sep># provair

[![Build Status](https://travis-ci.com/provair/provair.svg?token=<KEY>&branch=master)](https://travis-ci.com/github/provair/provair)
[![codecov](https://codecov.io/gh/provair/provair/branch/master/graph/badge.svg?token=<KEY>)](https://codecov.io/gh/provair/provair)

> A provable fair system that can pass all
> [SP 800-22](https://csrc.nist.gov/publications/detail/sp/800-22/rev-1a/final)
> tests.

## Install

```sh
npm i provair
```

## Usage

**TBD**

## SP 800-22 testing

[sp800_22_tests](https://github.com/dj-on-github/sp800_22_tests) is A python
implementation of the SP800-22 Rev 1a PRNG test suite.

```sh
$ npm i @provair/cli -g
$ git clone https://github.com/dj-on-github/sp800_22_tests
$ cd sp800_22_tests
$ provair rand -b -k 256 -o test.bin
$ python ./sp800_22_tests.py ./test.bin

Tests of Distinguishability from Random
TEST: monobit_test
  Ones count   = 1048307
  Zeroes count = 1048845
  PASS
  P=0.710259739353

[ Lots of per test output ]

SUMMARY
-------
monobit_test                             0.710259739353     PASS
frequency_within_block_test              0.765249647678     PASS
runs_test                                0.537860435367     PASS
longest_run_ones_in_a_block_test         0.403983546931     PASS
binary_matrix_rank_test                  0.200488689692     PASS
dft_test                                 0.903163489064     PASS
non_overlapping_template_matching_test   0.999999473971     PASS
overlapping_template_matching_test       0.277789765986     PASS
maurers_universal_test                   0.14380305027      PASS
linear_complexity_test                   0.288352301634     PASS
serial_test                              0.731698804316     PASS
approximate_entropy_test                 0.73145286156      PASS
cumulative_sums_test                     0.553723086962     PASS
random_excursion_test                    0.250402086143     PASS
random_excursion_variant_test            0.25288752955      PASS
```

The details result can be found [here](SP800-22.txt)

## Licence

[Apache-2.0](LICENSE)
<file_sep># @provair/cli

> The command line tool of provair
<file_sep>const random = require('bcrypto/lib/random');

export const DEFAULT_RANDOM_HEX_BYTES = 16;
export const DEFAULT_CLIENT_SEED_BYTES = 21;
export const DEFAULT_SERVER_SEED_BYTES = 16;

export const AlphabetCharacters =
  'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

export const NumberCharacters = '0123456789';

export const AlphabetAndNumberCharacters =
  AlphabetCharacters + NumberCharacters;

export function randomString(
  length: number,
  characters = AlphabetAndNumberCharacters,
) {
  let string = '';
  // Generate the string
  const charsLen = characters.length;
  const maxByte = 256 - (256 % charsLen);
  while (length > 0) {
    const buf = random.randomBytes(Math.ceil((length * 256) / maxByte));
    for (let i = 0; i < buf.length && length > 0; i++) {
      const randomByte = buf.readUInt8(i);
      if (randomByte < maxByte) {
        string += characters[randomByte % charsLen];
        length--;
      }
    }
  }

  return string;
}

export function randomHex(bytes = DEFAULT_RANDOM_HEX_BYTES) {
  return random.randomBytes(bytes).toString('hex').slice();
}

export function genClientSeed(length = DEFAULT_CLIENT_SEED_BYTES) {
  return randomString(length);
}

export function genServerSeed(bytes = DEFAULT_SERVER_SEED_BYTES) {
  return randomHex(bytes);
}
<file_sep>import {BufferWriter} from 'memstreams';
import {expect} from '@loopback/testlab';
import {Randomizer} from '../randomizer';

describe('randomizer', function () {
  it('should generate default size random bytes', async function () {
    const data = await new Promise(resolve => {
      const randomizer = new Randomizer();
      const writer = new BufferWriter();
      randomizer.pipe(writer);
      writer.on('finish', () => {
        resolve(writer.data);
      });
    });
    expect(data).lengthOf(1024);
  });

  it('should generate specified size random bytes', async function () {
    const data = await new Promise(resolve => {
      const randomizer = new Randomizer({size: 2048});
      const writer = new BufferWriter();
      randomizer.pipe(writer);
      writer.on('finish', () => {
        resolve(writer.data);
      });
    });
    expect(data).lengthOf(2048);
  });

  it('should work with low high water mark', async function () {
    const data = await new Promise(resolve => {
      const randomizer = new Randomizer({highWaterMark: 199});
      const writer = new BufferWriter();
      randomizer.pipe(writer);
      writer.on('finish', () => {
        resolve(writer.data);
      });
    });
    expect(data).lengthOf(1024);
  });
});
<file_sep>import {Strategy} from '../types';

import seedrandom = require('seedrandom');

export class ARC4Strategy implements Strategy {
  readonly name = 'arc4';

  calc(data: Buffer | string): number {
    const hex = Buffer.isBuffer(data) ? data.toString('hex') : data;
    const value = seedrandom(hex)() * 100;
    return Math.floor(value * 10000) / 10000;
  }
}

export default ARC4Strategy;
<file_sep>import {Strategy, StrategyCtor} from './types';

const debug = require('debug')('provair:factory');

export type ModuleLoader<T> = (name: string) => T | undefined;

export function isStrategy(x: any): x is Strategy {
  return typeof x === 'object' && typeof x.calc === 'function';
}

// List possible strategy module names
function strategyModuleNames(name: string) {
  const names = []; // Check the name as is
  if (!name.match(/^\//)) {
    names.push('./strategies/' + name); // Check built-in strategies
    if (name.indexOf('provair-strategy-') !== 0) {
      names.push('provair-strategy-' + name); // Try provair-strategy-<name>
    }
  }
  return names;
}

// testable with DI
function tryModules<T>(
  names: string[],
  loader?: ModuleLoader<T>,
): T | undefined {
  let mod: T | undefined = undefined;
  loader = loader ?? require;
  for (const name of names) {
    try {
      mod = loader(name);
    } catch (e) {
      const notFound =
        e.code === 'MODULE_NOT_FOUND' &&
        e.message &&
        e.message.indexOf(name) > 0;

      if (notFound) {
        debug('Module %s not found, will try another candidate.', name);
        continue;
      }

      debug('Cannot load strategy %s: %s', name, e.stack || e);
      throw e;
    }
    if (mod) {
      break;
    }
  }
  return mod;
}

const NotInstalledMessage = (name: string, names: string[]) => `
WARNING: Provair strategy "${name}" is not installed as any of the following modules:
 
${names.join('\n')}

To fix, run:
    
    npm install ${names[names.length - 1]}
`;

export function resolveStrategyCtor(
  name: string,
  loader?: ModuleLoader<StrategyCtor>,
): StrategyCtor | Error {
  const names = strategyModuleNames(name);
  let strategy: any = tryModules(names, loader);
  strategy = strategy?.default ?? strategy;
  let error = null;
  if (!strategy) {
    error = new Error(NotInstalledMessage(name, names));
  }
  return strategy ?? error;
}

export function resolveStrategy(
  name: string,
  loader?: ModuleLoader<StrategyCtor>,
): Strategy;
export function resolveStrategy(strategy: Strategy): Strategy;
export function resolveStrategy(
  strategy: StrategyCtor,
  options?: Record<string, any>,
): Strategy;
export function resolveStrategy(
  nameOrStrategy: string | StrategyCtor | Strategy,
  loaderOrOptions?: ModuleLoader<StrategyCtor> | Record<string, any>,
): Strategy {
  if (isStrategy(nameOrStrategy)) {
    return nameOrStrategy;
  }
  let name: string | undefined;
  let loader: ModuleLoader<StrategyCtor> | undefined;
  let options: Record<string, any> | undefined;
  if (typeof loaderOrOptions === 'function') {
    loader = <ModuleLoader<StrategyCtor>>loaderOrOptions;
  } else {
    options = loaderOrOptions;
  }
  let ctor: StrategyCtor;
  if (typeof nameOrStrategy === 'string') {
    name = nameOrStrategy;
    const resolved = resolveStrategyCtor(nameOrStrategy, loader);
    if (resolved instanceof Error) {
      throw resolved;
    }
    ctor = resolved;
  } else {
    ctor = nameOrStrategy;
  }

  try {
    return new ctor(options);
  } catch (err) {
    if (err.message) {
      err.message =
        'Cannot construct strategy ' +
        JSON.stringify(name ?? ctor.name) +
        ': ' +
        err.message;
    }
    throw err;
  }
}
<file_sep>import {SHA256} from './algs';
import {randomHex} from './utils';

export function generateServerSeed(
  seedOrLength: Buffer | string | number = randomHex(16),
) {
  let seed: Buffer;
  if (typeof seedOrLength === 'number') {
    seed = randomHex(seedOrLength);
  } else if (typeof seedOrLength === 'string') {
    seed = Buffer.from(seedOrLength, 'hex');
  } else {
    seed = seedOrLength;
  }
  const hash = SHA256.digest(seed).toString('hex');
  return {seed: seed.toString('hex'), hash};
}
<file_sep>export * as rand from './rand';
<file_sep># @provair/example-server

> A provair server example
<file_sep>import {expect} from '@loopback/testlab';
import {resolveStrategy} from '../strategies';
import {Strategy} from '../types';
import {MockLoader} from './mocks/mock-loader';

describe('strategies', function () {
  it('should reports helpful error when strategy init throws', function () {
    class ThrowingStrategy implements Strategy {
      constructor() {
        throw new Error('expected test error');
      }

      calc(hex: Buffer | string): number {
        return 0;
      }
    }

    expect(() => resolveStrategy(ThrowingStrategy)).throw(/ThrowingStrategy/);
  });

  it('should report helpful error if module not found', function () {
    expect(() => resolveStrategy('unknown')).throw(/not installed/);
  });

  it('should load strategy with custom loader', function () {
    const strategy = resolveStrategy('mock', MockLoader.load);
    expect(strategy).ok();
  });

  it('should report cannot load strategy if strategy module throw unexpected error when loading', function () {
    expect(() =>
      resolveStrategy('unknown', name => {
        throw new Error('load error');
      }),
    ).throw(/load error/);
  });
});
<file_sep>export * from './provably.service';
<file_sep>import {BootMixin} from '@loopback/boot';
import {ApplicationConfig} from '@loopback/core';
import {RestApplication} from '@loopback/rest';
import {RestExplorerComponent} from '@loopback/rest-explorer';

export class ProvablyApplication extends BootMixin(RestApplication) {
  constructor(config: ApplicationConfig = {}) {
    super(config);
    this.projectRoot = __dirname;

    this.component(RestExplorerComponent);

    this.bootOptions = {
      controllers: {
        // Customize ControllerBooter Conventions here
        dirs: ['controllers'],
        extensions: ['.controller.js', '.controller.ts'],
        nested: true,
      },
      services: {
        // Customize ServiceBooter Conventions here
        dirs: ['services'],
        extensions: ['.service.js', '.service.ts'],
        nested: true,
      },
    };
  }

  async main() {
    await this.boot();
    await this.start();
  }
}
<file_sep>const random = require('bcrypto/lib/random');

export const DEFAULT_RANDOM_HEX_BYTES = 16;
export const DEFAULT_RANDOM_HEX_STRING_BYTES = 16;

export function randomHex(bytes = DEFAULT_RANDOM_HEX_BYTES) {
  return random.randomBytes(bytes);
}

export function randomHexStr(bytes = DEFAULT_RANDOM_HEX_STRING_BYTES) {
  return randomHex(bytes).toString('hex').slice();
}
<file_sep>import {expect} from '@loopback/testlab';
import {ARC4Strategy} from '../../strategies/arc4';

const arc4 = new ARC4Strategy();

describe('strategy/arc4', function () {
  it('should calculate number', function () {
    const data = 'F000112345';
    const v1 = arc4.calc(data);
    const v2 = arc4.calc(data);
    expect(v1).equal(v2);
    expect(v1).greaterThanOrEqual(0);
    expect(v1).lessThanOrEqual(100);
  });

  it('should receive both buffer and string', function () {
    const data = 'F000112345';
    const resultWithString = arc4.calc(data.toLowerCase());
    const resultWithBuffer = arc4.calc(Buffer.from(data, 'hex'));
    expect(resultWithBuffer).equal(resultWithString);
  });

  it('should return different value with different cases', function () {
    const data = 'F000112345';
    const resultWithString = arc4.calc(data); // upper case
    const resultWithBuffer = arc4.calc(Buffer.from(data, 'hex')); // lower case
    expect(resultWithBuffer).not.equal(resultWithString);
  });
});
<file_sep>import {BindingKey} from '@loopback/core';
import {ProvablyService} from './services';

/**
 * Strongly-typed binding key for CachingService
 */
export const PROVABLY_SERVICE = BindingKey.create<ProvablyService>(
  'services.ProvablyService',
);
<file_sep>import {StrategyCtor} from '../../types';
import {MockStrategy} from './mock-strategy';

export namespace MockLoader {
  const strategies: Record<string, any> = {};

  export function register(name: string, strategy: StrategyCtor) {
    strategies[name] = strategy;
  }

  export function load(name: string) {
    if (strategies[name]) {
      return strategies[name];
    }
    const err = <any>new Error(`Cannot find module ${name}.`);
    err.code = 'MODULE_NOT_FOUND';
    throw err;
  }
}

MockLoader.register('provair-strategy-mock', MockStrategy);
