From 4b79df0917df0c5d3b4a8fd5df1938c92159a5d3 Mon Sep 17 00:00:00 2001 From: Brandon Belvin Date: Wed, 23 Nov 2016 17:25:30 -0600 Subject: [PATCH] Migrate to Typescript --- .eslintrc | 6 +- .gitignore | 1 + .npmignore | 7 +- .travis.yml | 12 +- README.md | 2 +- npm-shrinkwrap.json | 10 +- package.json | 35 ++++-- src/concealer.js | 105 ------------------ src/concealer.ts | 60 ++++++++++ test/concealer.test.js | 244 ----------------------------------------- test/concealer.test.ts | 165 ++++++++++++++++++++++++++++ tsconfig.build.json | 23 ++++ tsconfig.json | 20 ++++ 13 files changed, 321 insertions(+), 369 deletions(-) delete mode 100644 src/concealer.js create mode 100644 src/concealer.ts delete mode 100644 test/concealer.test.js create mode 100644 test/concealer.test.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json diff --git a/.eslintrc b/.eslintrc index b1a3270..9fcb0e7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,7 +3,11 @@ "node": true }, - "extends": "eslint:recommended", + "parser": "typescript-eslint-parser", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, "rules": { "no-console": 2, diff --git a/.gitignore b/.gitignore index 6732e89..588afcd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ artifacts/ docs/ .nyc_output/ logs/ +dist/ *.log *.tap diff --git a/.npmignore b/.npmignore index 327b5e1..358548e 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,7 @@ -circle.yml +.travis.yml test/ +src/ +coverage/ +tsconfig.json +tsconfig.build.json +artifacts/ diff --git a/.travis.yml b/.travis.yml index 325bde6..223ebd7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,16 @@ language: node_js node_js: - "4" - - "0.12" - - "0.10" + - "6" - "node" +install: + - npm install + +script: + - npm run test-ci + +after_success: + - './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls' + sudo: false diff --git a/README.md b/README.md index 6a4d4a7..410471d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A fast two-way encryption module to generate unique, random-appearing, non-sequential strings from integers. This is a great way to encode database primary keys before presenting them to the user. -[![Build Status](https://travis-ci.org/zefferus/concealer.svg?branch=master)](https://travis-ci.org/zefferus/concealer)![Current Version](https://img.shields.io/npm/v/concealer.svg) +[![Build Status](https://travis-ci.org/zefferus/concealer.svg?branch=master)](https://travis-ci.org/zefferus/concealer) [![Coverage Status](https://coveralls.io/repos/github/zefferus/concealer/badge.svg?branch=master)](https://coveralls.io/github/zefferus/concealer?branch=master) [![Current Version](https://img.shields.io/npm/v/concealer.svg)](https://www.npmjs.com/package/concealer) Development on **Concealer** is sponsored by [Sparo Labs](http://www.sparolabs.com/). diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 0507ab7..52bfa58 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,15 +1,15 @@ { "name": "concealer", - "version": "1.0.0", + "version": "2.0.0", "dependencies": { "hashids": { - "version": "1.0.2", - "from": "hashids@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/hashids/-/hashids-1.0.2.tgz" + "version": "1.1.1", + "from": "hashids@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/hashids/-/hashids-1.1.1.tgz" }, "skip32": { "version": "1.2.1", - "from": "skip32@latest", + "from": "skip32@>=1.2.1 <2.0.0", "resolved": "https://registry.npmjs.org/skip32/-/skip32-1.2.1.tgz" } } diff --git a/package.json b/package.json index 7e89670..57fc562 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,23 @@ { "name": "concealer", - "version": "1.0.0", + "version": "2.0.0", "description": "A primary key encoding utility", - "main": "src/concealer.js", + "main": "dist/concealer.js", "engines": { - "node": ">=0.10.0" + "node": ">=4.0" }, "scripts": { - "lint": "node_modules/.bin/eslint src", - "test": "node_modules/.bin/tap --cov --reporter=spec test/**/*.test.js", - "precoverage": "node_modules/.bin/rimraf coverage", - "coverage": "node_modules/.bin/tap --coverage-report=lcov" + "lint": "eslint src/**/*.ts", + "pretest": "rimraf dist && tsc", + "test": "nyc --cache ava --tap | tap-nyan", + "posttest": "nyc report --check-coverage --statements=90", + "coverage:report": "nyc report --reporter=lcov && opener ./coverage/lcov-report/index.html", + "pretest-ci": "rimraf dist && tsc", + "test-ci": "nyc --cache ava --verbose", + "posttest-ci": "nyc report --check-coverage --statements=90", + "prebuild": "npm run lint && npm run test && rimraf dist", + "build": "tsc -p tsconfig.build.json --sourceMap false -d", + "prepackage": "npm run build" }, "repository": { "type": "git", @@ -29,13 +36,21 @@ "url": "https://github.com/zefferus/concealer/issues" }, "homepage": "https://github.com/zefferus/concealer#readme", + "typings": "dist/concealer.d.ts", "devDependencies": { - "eslint": "^2.4.0", + "@types/node": "^6.0.51", + "ava": "^0.17.0", + "coveralls": "^2.11.15", + "eslint": "^3.10.2", + "nyc": "^10.0.0", + "opener": "^1.4.2", "rimraf": "^2.5.2", - "tap": "^5.7.0" + "tap-nyan": "^1.1.0", + "typescript": "^2.1.1", + "typescript-eslint-parser": "^1.0.0" }, "dependencies": { - "hashids": "^1.0.2", + "hashids": "^1.1.1", "skip32": "^1.2.1" } } diff --git a/src/concealer.js b/src/concealer.js deleted file mode 100644 index fc672f6..0000000 --- a/src/concealer.js +++ /dev/null @@ -1,105 +0,0 @@ -'use strict'; - -var Skip32 = require('skip32').Skip32; -var Hashids = require('hashids'); - -var internals = { - isInteger: Number.isInteger -}; - -// Number.isInteger is ES2016, include this polyfill just in case -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger - -// istanbul ignore if -if (!internals.isInteger) { - internals.isInteger = function isInteger(value) { - return typeof value === 'number' && - isFinite(value) && - Math.floor(value) === value; - }; -} - -// Returns whether a given value represents a single byte -internals.isByte = function isByte(val) { - return internals.isInteger(val) && val >= 0 && val <= 255; -}; - - -// Public methods - -/** - * Creates new Concealer utility. It iss highly recommended to provide a secret - * key of 10 bytes to achieve the best encryption. - * - * @param {Array} secretKey - Array of bytes for the secret key. - * @param {string} salt - Salt for hashing function. - * @param {?number} [minLength] - Minimum length of encoder output. - * @param {?string} [customAlphabet] - Custom alphabet to use for generating - * hash results. - * @throws {Error|TypeError} If Concealer isn't instantiated as new or if - * secretKey, salt, or minLength are not provided or are not of right types. - */ -exports = module.exports = internals.Concealer = Concealer; -function Concealer(secretKey, salt, minLength, customAlphabet) { - if (!(this instanceof internals.Concealer)) { - throw new Error('Concealer must be instantiated using new'); - } - - if (!secretKey) { - throw new Error('Secret Key must be provided'); - } - if (!salt) { - throw new Error('Salt must be provided'); - } - if (!Array.isArray(secretKey) || !secretKey.length || - !secretKey.every(internals.isByte)) { - - throw new TypeError('Secret Key must be an Array of bytes represented by integers'); - } - if (minLength !== undefined && !internals.isInteger(minLength)) { - throw new TypeError('Min Length must be an integer, if provided'); - } - - this._skip32 = new Skip32(secretKey); - this._hashids = new Hashids(salt, minLength || 1, customAlphabet); -} - - -/** - * Encrypts and encodes an integer to an obfuscated string. - * - * @param {number} key - Non-negative integer to encode - * @return {string} Encrypted and encoded key - * @throws {TypeError} If key is not a non-negative integer - */ -internals.Concealer.prototype.encode = function encode(key) { - if (!internals.isInteger(key) || key < 0) { - throw new TypeError('Key must be a non-negative integer'); - } - - var encrypted = this._skip32.encrypt(key); - - return this._hashids.encode(encrypted); -}; - - -/** - * Decrypts encoded key back to a number. - * - * @param {string} key - The encoded key. - * @return {number|null} Key ID or null if the key could not be decoded. - * @throws {TypeError} If key is not a string. - */ -internals.Concealer.prototype.decode = function decode(key) { - if (typeof key !== 'string') { - throw new TypeError('Key must be a string'); - } - - var encrypted = this._hashids.decode(key)[0]; - - if (encrypted === undefined) { - return null; - } - - return this._skip32.decrypt(encrypted); -}; diff --git a/src/concealer.ts b/src/concealer.ts new file mode 100644 index 0000000..477a728 --- /dev/null +++ b/src/concealer.ts @@ -0,0 +1,60 @@ +var Skip32: any = require('skip32').Skip32; +var Hashids: any = require('hashids'); + +// Public methods + +export class Concealer { + + private skip32: any; + private hashids: any; + + constructor(private secretKey: number[], private salt: string, + private minLength = 1, private customAlphabet?: string) { + + if (!secretKey) { + throw new Error('Secret Key must be provided'); + } + if (!salt) { + throw new Error('Salt must be provided'); + } + if (!Array.isArray(secretKey) || !secretKey.length || !secretKey.every(isByte)) { + throw new TypeError('Secret Key must be an Array of bytes represented by integers'); + } + if (minLength !== undefined && !Number.isInteger(minLength)) { + throw new TypeError('Min Length must be an integer, if provided'); + } + + this.skip32 = new Skip32(secretKey); + this.hashids = new Hashids(salt, minLength || 1, customAlphabet); + } + + + encode(key: number): string { + if (!Number.isInteger(key) || key < 0) { + throw new TypeError('Key must be a non-negative integer'); + } + + const encrypted = this.skip32.encrypt(key); + + return this.hashids.encode(encrypted); + } + + + decode(key: string): number { + const encrypted = this.hashids.decode(key)[0]; + + if (encrypted === undefined) { + return null; + } + + return this.skip32.decrypt(encrypted); + } +} + + +// Private methods + +// Returns whether a given value represents a single byte +function isByte(val: number) { + return Number.isInteger(val) && val >= 0 && val <= 255; +}; diff --git a/test/concealer.test.js b/test/concealer.test.js deleted file mode 100644 index 51cc34a..0000000 --- a/test/concealer.test.js +++ /dev/null @@ -1,244 +0,0 @@ -'use strict'; - -/* eslint func-names: 0, max-len: 0, no-unused-vars: 0 */ - -var tap = require('tap'); -var test = tap.test; - -var Concealer = require('../src/concealer'); - -var config = [{ - key: [ 0x0f, 0x46, 0xd2, 0xde, 0x2d, 0x23, 0x8b, 0xe3, 0x07, 0x22 ], - salt: 'EcQxgFe2xMLFsqJPclHN 2M5AFgL7WRoQP5F2vcE3', - minlength: 20 -}, { - key: [ 0xf0, 0x64, 0x2d, 0xed, 0xd2, 0x32, 0xb8, 0x3e, 0x70, 0x22 ], - salt: 'EcQxgFe2 2M5AFgL7WRoQP', - minlength: 5 -}]; - -test('Errors if not instantiated new', function (t) { - t.plan(1); - - t.throws(function () { - var concealer = Concealer(config[0].key, config[0].salt, config[0].minlength); - }); - t.end(); -}); - -test('Fails with no salt provided', function (t) { - t.plan(2); - - t.throws(function () { - new Concealer([ 0x0f, 0x46 ], null, 10); - }, Error); - t.throws(function () { - new Concealer([ 0x0f, 0x46 ], null); - }, Error); - - t.end(); -}); - -test('Fails with no secret key provided', function (t) { - t.plan(3); - - t.throws(function () { - new Concealer(); - }, Error); - t.throws(function () { - new Concealer(null, 'salt'); - }, Error); - t.throws(function () { - new Concealer(null, 'salt', 10); - }, Error); - - t.end(); -}); - -test('Fails with bad secret key', function (t) { - t.plan(5); - - t.throws(function () { - new Concealer('badkey', 'salt'); - }, TypeError); - t.throws(function () { - new Concealer([], 'salt'); - }, TypeError); - t.throws(function () { - new Concealer([ 1, 2, 3, '1' ], 'salt'); - }, TypeError); - t.throws(function () { - new Concealer([ -1 ], 'salt'); - }, TypeError); - t.throws(function () { - new Concealer([ 0, 1, 256, 255 ], 'salt'); - }, TypeError); - - t.end(); -}); - -test('Fails with bad min length', function (t) { - t.plan(2); - - t.throws(function () { - new Concealer(config[0].key, config[0].salt, 0.5); - }, TypeError); - t.throws(function () { - new Concealer(config[0].key, config[0].salt, '1'); - }, TypeError); - - t.end(); -}); - -test('Fails on encode non-integer', function (t) { - t.plan(2); - - var concealer = new Concealer(config[0].key, config[0].salt, config[0].minlength); - - t.throws(function () { - concealer.encode('xyz'); - }, TypeError); - t.throws(function () { - concealer.encode(1.5); - }, TypeError); - - t.end(); -}); - -test('Fails on decode non-string', function (t) { - t.plan(1); - - var concealer = new Concealer(config[0].key, config[0].salt, config[0].minlength); - - t.throws(function () { - concealer.decode(123); - }, TypeError); - - t.end(); -}); - -test('Encodes id with minimum length', function (t) { - t.plan(3); - - var concealer = new Concealer(config[0].key, config[0].salt, config[0].minlength); - - var id = 100; - var encoded = concealer.encode(id); - - t.notEquals(id, encoded); - t.ok(typeof encoded === 'string'); - t.ok(encoded.length >= config[0].minlength); - - t.end(); -}); - -test('Encodes id without minimum length', function (t) { - t.plan(2); - - var concealer = new Concealer(config[0].key, config[0].salt); - - var id = 100; - var encoded = concealer.encode(id); - - t.notEquals(id, encoded); - t.ok(typeof encoded === 'string'); - - t.end(); -}); - -test('Null on bad decode string', function (t) { - t.plan(1); - - var concealer = new Concealer(config[0].key, config[0].salt, config[0].minlength); - - var decoded = concealer.decode('this is an obviously bad decode string'); - - t.equals(null, decoded); - - t.end(); -}); - -test('Decodes id', function (t) { - t.plan(1); - - var concealer = new Concealer(config[0].key, config[0].salt, config[0].minlength); - - var id = 101; - var encoded = concealer.encode(id); - var decoded = concealer.decode(encoded); - - t.equals(id, decoded); - - t.end(); -}); - -test('Same id with same secrets produces same encoding', function (t) { - t.plan(1); - - var concealer = new Concealer(config[0].key, config[0].salt, config[0].minlength); - - var id1 = 5000; - var id2 = 5000; - - var encoded1 = concealer.encode(id1); - var encoded2 = concealer.encode(id2); - - t.equals(encoded1, encoded2); - - t.end(); -}); - -test('Same id with different secrets produces same encoding', function (t) { - t.plan(1); - - var concealer1 = new Concealer(config[0].key, config[0].salt, config[0].minlength); - var concealer2 = new Concealer(config[1].key, config[1].salt, config[1].minlength); - - var id1 = 5000; - var id2 = 5000; - - var encoded1 = concealer1.encode(id1); - var encoded2 = concealer2.encode(id2); - - t.notEquals(encoded1, encoded2); - - t.end(); -}); - -test('Different keys produce different encodings', function (t) { - t.plan(1); - - var concealer = new Concealer(config[0].key, config[0].salt, config[0].minlength); - - var key1 = 1000; - var key2 = 1001; - - var encoded1 = concealer.encode(key1); - var encoded2 = concealer.encode(key2); - - t.notEquals(encoded1, encoded2); - - t.end(); -}); - -test('Same id, same secrets, diff minlength produces different encodings that both decode', function (t) { - t.plan(2); - - var concealer1 = new Concealer(config[0].key, config[0].salt, 0); - var concealer2 = new Concealer(config[0].key, config[0].salt, config[0].minlength + 200); - - var id1 = 5000; - var id2 = 5000; - - var encoded1 = concealer1.encode(id1); - var encoded2 = concealer2.encode(id2); - - t.notEquals(encoded1, encoded2); - - var decoded1 = concealer2.decode(encoded1); - var decoded2 = concealer1.decode(encoded2); - - t.equals(decoded1, decoded2); - - t.end(); -}); diff --git a/test/concealer.test.ts b/test/concealer.test.ts new file mode 100644 index 0000000..f368425 --- /dev/null +++ b/test/concealer.test.ts @@ -0,0 +1,165 @@ +'use strict'; + +/* eslint func-names: 0, max-len: 0, no-unused-consts: 0 */ + +import test from 'ava'; + +import { Concealer } from '../src/concealer.js'; + +const config = [{ + key: [ 0x0f, 0x46, 0xd2, 0xde, 0x2d, 0x23, 0x8b, 0xe3, 0x07, 0x22 ], + salt: 'EcQxgFe2xMLFsqJPclHN 2M5AFgL7WRoQP5F2vcE3', + minlength: 20 +}, { + key: [ 0xf0, 0x64, 0x2d, 0xed, 0xd2, 0x32, 0xb8, 0x3e, 0x70, 0x22 ], + salt: 'EcQxgFe2 2M5AFgL7WRoQP', + minlength: 5 +}]; + +test('Fails with no salt provided', (t) => { + t.plan(2); + + t.throws(() => { new Concealer([ 0x0f, 0x46 ], null, 10); }, Error); + t.throws(() => { new Concealer([ 0x0f, 0x46 ], null); }, Error); +}); + +test('Fails with no secret key provided', (t) => { + t.plan(2); + + t.throws(() => { new Concealer(null, 'salt'); }, Error); + t.throws(() => { new Concealer(null, 'salt', 10); }, Error); +}); + +test('Fails with bad secret key', (t) => { + t.plan(3); + + t.throws(() => { new Concealer([], 'salt'); }, TypeError); + t.throws(() => { new Concealer([ -1 ], 'salt'); }, TypeError); + t.throws(() => { new Concealer([ 0, 1, 256, 255 ], 'salt'); }, TypeError); +}); + +test('Fails with bad min length', (t) => { + t.plan(2); + + t.throws(() => { new Concealer(config[0].key, config[0].salt, 0.5); }, TypeError); + t.throws(() => { new Concealer(config[0].key, config[0].salt, null); }, TypeError); +}); + +test('Fails on encode non-positive integer', (t) => { + t.plan(2); + + const concealer = new Concealer(config[0].key, config[0].salt, config[0].minlength); + + t.throws(() => { concealer.encode(-1); }, TypeError); + t.throws(() => { concealer.encode(1.5); }, TypeError); +}); + +test('Encodes id with minimum length', (t) => { + t.plan(3); + + const concealer = new Concealer(config[0].key, config[0].salt, config[0].minlength); + + const id = 100; + const encoded = concealer.encode(id); + + t.not(id.toString(), encoded); + t.truthy(typeof encoded === 'string'); + t.truthy(encoded.length >= config[0].minlength); +}); + +test('Encodes id without minimum length', (t) => { + t.plan(2); + + const concealer = new Concealer(config[0].key, config[0].salt); + + const id = 100; + const encoded = concealer.encode(id); + + t.not(id.toString(), encoded); + t.truthy(typeof encoded === 'string'); +}); + +test('Null on bad decode string', (t) => { + t.plan(1); + + const concealer = new Concealer(config[0].key, config[0].salt, config[0].minlength); + + const decoded = concealer.decode('this is an obviously bad decode string'); + + t.is(null, decoded); +}); + +test('Decodes id', (t) => { + t.plan(1); + + const concealer = new Concealer(config[0].key, config[0].salt, config[0].minlength); + + const id = 101; + const encoded = concealer.encode(id); + const decoded = concealer.decode(encoded); + + t.is(id, decoded); +}); + +test('Same id with same secrets produces same encoding', (t) => { + t.plan(1); + + const concealer = new Concealer(config[0].key, config[0].salt, config[0].minlength); + + const id1 = 5000; + const id2 = 5000; + + const encoded1 = concealer.encode(id1); + const encoded2 = concealer.encode(id2); + + t.is(encoded1, encoded2); +}); + +test('Same id with different secrets produces same encoding', (t) => { + t.plan(1); + + const concealer1 = new Concealer(config[0].key, config[0].salt, config[0].minlength); + const concealer2 = new Concealer(config[1].key, config[1].salt, config[1].minlength); + + const id1 = 5000; + const id2 = 5000; + + const encoded1 = concealer1.encode(id1); + const encoded2 = concealer2.encode(id2); + + t.not(encoded1, encoded2); +}); + +test('Different keys produce different encodings', (t) => { + t.plan(1); + + const concealer = new Concealer(config[0].key, config[0].salt, config[0].minlength); + + const key1 = 1000; + const key2 = 1001; + + const encoded1 = concealer.encode(key1); + const encoded2 = concealer.encode(key2); + + t.not(encoded1, encoded2); +}); + +test('Same id, same secrets, diff minlength produces different encodings that both decode', (t) => { + t.plan(2); + + const concealer1 = new Concealer(config[0].key, config[0].salt, 0); + const concealer2 = new Concealer(config[0].key, config[0].salt, config[0].minlength + 200); + + const id1 = 5000; + const id2 = 5000; + + const encoded1 = concealer1.encode(id1); + const encoded2 = concealer2.encode(id2); + + t.not(encoded1, encoded2); + + const decoded1 = concealer2.decode(encoded1); + const decoded2 = concealer1.decode(encoded2); + + t.is(decoded1, decoded2); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..0aad5d7 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "sourceMap": true, + "noImplicitAny": true, + "removeComments": true, + "preserveConstEnums": true, + "outDir": "./dist", + "target": "es5", + "lib": [ + "es5", + "es2015" + ] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "dist", + "coverage", + "node_modules" + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..dc81a38 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "commonjs", + "sourceMap": true, + "noImplicitAny": true, + "removeComments": true, + "preserveConstEnums": true, + "outDir": "./dist", + "target": "es5", + "lib": [ + "es5", + "es2015" + ] + }, + "exclude": [ + "dist", + "coverage", + "node_modules" + ] +} \ No newline at end of file