Initial commit

This commit is contained in:
Andrew Sliwinski 2016-11-03 17:10:04 -04:00
parent 9d5423f22a
commit 3534c204d0
16 changed files with 472 additions and 9 deletions

3
.eslintignore Normal file
View file

@ -0,0 +1,3 @@
node_modules/*
coverage/*
.nyc_output/*

10
.eslintrc.js Normal file
View file

@ -0,0 +1,10 @@
module.exports = {
extends: ['scratch', 'scratch/node'],
env: {
node: true,
es6: true
},
rules: {
'no-div-regex': [0]
}
};

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
# MacOS
.DS_Store
# NPM
/node_modules
npm-*
# Code coverage
/coverage
/.nyc_output

5
.npmrc
View file

@ -1,5 +0,0 @@
engine-strict=true
save-exact=true
save-prefix=~
init-license=BSD-3-Clause
init-author-name=Massachusetts Institute of Technology

8
.travis.yml Normal file
View file

@ -0,0 +1,8 @@
language: node_js
node_js:
- 'stable'
- 'latest'
sudo: false
cache:
directories:
- node_modules

16
README.md Normal file
View file

@ -0,0 +1,16 @@
## scratch-auth
#### Utilities for authenticating Scratch users.
## Installation
```bash
npm install git@github.com:LLK/scratch-auth.git
```
## Basic Use
```js
const Auth = require('scratch-auth');
const a = new Auth('salt', 'secret');
const unsigned = a.unsign('sometoken');
const unpacked = a.unpack(unsigned);
```

72
index.js Normal file
View file

@ -0,0 +1,72 @@
const pako = require('pako');
const signer = require('./lib/signer');
const util = require('./lib/util');
class Auth {
constructor (salt, secret) {
this.salt = salt;
this.secret = secret;
}
/**
* Port of django.core.signing.signer.unsign
* @param {string} signedString String in the form value:time:signature
* @return {string} Unsigned string
*/
unsign (signedString) {
// Validate
if (typeof signedString !== 'string') return;
if (signedString.indexOf(':') === -1) return;
// Decode
var components = signedString.split(':');
var value = components.slice(0, -1).join(':');
var signature = components.slice(-1)[0];
var challenge = signer.base64Hmac(this.salt, value, this.secret);
// Compare signature to challenge
if (util.md5(signature) !== util.md5(challenge)) return;
return value;
}
/**
* Return the usable content portion of a signed, compressed cookie
* generated by Django's signing module
* See: github.com/django/django/blob/stable/1.8.x/django/core/signing.py
* @param {string} s Signed (and optionally compressed) cookie
* @return {object} Unpacked cookie
*/
unpack (s) {
// Validate
if (typeof s !== 'string') return;
// Storage objects
const decompress = (s[0] === '.');
const b64data = s.split(':')[0];
// Base64 decode
var result = util.b64Decode(b64data);
try {
// Handle decompression
if (decompress) {
var charData = result.split('').map(function (c) {
return c.charCodeAt(0);
});
var binData = new Uint8Array(charData);
var data = pako.inflate(binData);
result = String.fromCharCode.apply(null, new Uint16Array(data));
}
// Convert to object
result = JSON.parse(result);
} catch (e) {
return;
}
return result;
}
}
module.exports = Auth;

65
lib/signer.js Normal file
View file

@ -0,0 +1,65 @@
const crypto = require('crypto');
const util = require('./util');
const signer = module.exports = {
/**
* Method for obtaining the key for the HMAC-SHA1.
* @param {string} salt Crypto salt
* @param {string} secret Crypto secret
* @return {Buffer} Binary SHA1 digest of the salt and secret
*/
getSaltedHmacKey: function (salt, secret) {
// Validate
if (typeof salt !== 'string') return;
if (typeof secret !== 'string') return;
// Create SHA1 hash
var keySha1Sum = crypto.createHash('sha1');
keySha1Sum.update(salt + secret, 'binary');
return keySha1Sum.digest('binary');
},
/**
* Port of django.utils.crypto.salted_hmac:
* Returns the HMAC-SHA1 of `value`, using a key generated from the
* specified salt and secret.
* @param {string} salt Crypto salt
* @param {string} value Value to be transformed
* @param {string} secret Crypto secret
* @return {object} HMAC object
*/
getSaltedHmac: function (salt, value, secret) {
// Validate
if (typeof salt !== 'string') return;
if (typeof value !== 'string') return;
if (typeof secret !== 'string') return;
// Return HMAC-SHA1 of the specified `value`
var hmac = crypto.createHmac(
'sha1',
new Buffer(signer.getSaltedHmacKey(salt, secret), 'binary')
);
hmac.update(value, 'binary');
return hmac;
},
/**
* Port of django.core.signing.base64_hmac:
* Returns a URL-safe Base64 encoded representation of the digest of the
* HMAC of `value`.
* @param {string} salt Crypto salt
* @param {string} value Value to be transformed
* @param {string} key Crypto secret
* @return {string} URL-safe and Base64 encoded digest
*/
base64Hmac: function (salt, value, key) {
// Validate
if (typeof salt !== 'string') return;
if (typeof value !== 'string') return;
if (typeof key !== 'string') return;
// Get salted HMAC digest and Base64 encode
var saltedHmac = signer.getSaltedHmac(salt + 'signer', value, key);
return util.b64Encode(saltedHmac.digest('binary'));
}
};

70
lib/util.js Normal file
View file

@ -0,0 +1,70 @@
const crypto = require('crypto');
module.exports = {
/**
* Port of Python's base64.urlsafe_b64encode: Returns a base64-encoded
* representation of s, made URL-safe by replacing + and / with - and _
* respectively. Additionally all trailing =s are stripped from the
* resulting value to mirror django.core.signing.b64_encode.
* @param {string} s Input string.
* @return {string} URL-safe encoded string.
*/
b64Encode: function (s) {
// Validate
if (typeof s !== 'string') return;
// Convert from binary to Base64
var b64String = Buffer(s, 'binary').toString('base64');
// Replace special characters with URL-safe alternates
return b64String.replace(
/[+/]/g,
function (c) {
return {'+': '-', '/': '_'}[c];
}
).replace(/=+$/, '');
},
/**
* Port of Python's base64.urlsafe_b64decode: Returns a base64-decoded
* representation of s, made URL-safe by replacing + and / with - and _
* respectively. Handles removal of trailing =s which are stripped from
* encoded values to mirror django.core.signing.b64_decode.
* @param {string} s Base64 encoded string.
* @return {string} Decoded string.
*/
b64Decode: function (s) {
// Validate
if (typeof s !== 'string') return;
// Trim leading "dot" character
if (s[0] === '.') s = s.substring(1);
// Replace encoded special characters
s = s.replace(
/[-_]/g,
function (c) {
return {'-': '+', '_': '/'}[c];
}
);
// Convert from Base64 to binary
return new Buffer(s, 'base64').toString('binary');
},
/**
* Creates an MD5 hash from the specified input string.
* @param {string} s Input string.
* @return {string} Hash.
*/
md5: function (s) {
// Validate
if (typeof s !== 'string') return;
// Create hash and return hex digest
return crypto
.createHash('md5')
.update(s)
.digest('hex');
}
};

View file

@ -1,16 +1,28 @@
{
"name": "<name>",
"name": "scratch-auth",
"version": "1.0.0",
"description": "",
"author": "Massachusetts Institute of Technology",
"license": "BSD-3-Clause",
"homepage": "https://github.com/LLK/<name>#readme",
"homepage": "https://github.com/LLK/scratch-auth#readme",
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/LLK/<name>.git"
"url": "git+ssh://git@github.com/LLK/scratch-auth.git"
},
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "npm run lint && npm run tap",
"lint": "eslint .",
"tap": "tap test/{unit,integration}/*.js",
"coverage": "tap test/{unit,integration}/*.js --coverage --coverage-report=lcov"
},
"devDependencies": {
"babel-eslint": "7.1.0",
"eslint": "3.9.1",
"eslint-config-scratch": "2.0.3",
"tap": "8.0.0"
},
"dependencies": {
"pako": "1.0.3"
}
}

5
test/.eslintrc.js Normal file
View file

@ -0,0 +1,5 @@
module.exports = {
rules: {
'no-undefined': [0]
}
};

27
test/fixtures/cases.json vendored Normal file
View file

@ -0,0 +1,27 @@
{
"valid": {
"salt": "test",
"secret": "secret",
"signed": "value:zyBNJHpGyml3X-RhCx0mbjLFzPs",
"unsigned": "value"
},
"invalidSalt": {
"salt": "hax0r",
"secret": "secret",
"signed": "value:zyBNJHpGyml3X-RhCx0mbjLFzPs"
},
"invalidSecret": {
"salt": "test",
"secret": "hax0r",
"signed": "value:zyBNJHpGyml3X-RhCx0mbjLFzPs"
},
"invalidToken": {
"salt": "test",
"secret": "secret",
"signed": "zyBNJHpGyml3X-RhCx0mbjLFzPs"
},
"missingToken": {
"salt": "test",
"secret": "secret"
}
}

View file

@ -0,0 +1,25 @@
const test = require('tap').test;
const Auth = require('../../index');
const object = {foo: 'bar'};
const encoded = 'eyJmb28iOiJiYXIifQ==';
test('spec', function (t) {
const a = new Auth();
t.type(Auth, 'function');
t.type(a, 'object');
t.type(a.unpack, 'function');
t.end();
});
test('uncompressed', function (t) {
const a = new Auth();
t.strictDeepEqual(a.unpack(encoded), object);
t.end();
});
test('invalid', function (t) {
const a = new Auth();
t.strictDeepEqual(a.unpack(undefined), undefined);
t.end();
});

View file

@ -0,0 +1,47 @@
const test = require('tap').test;
const Auth = require('../../index');
const cases = require('../fixtures/cases');
test('spec', function (t) {
const a = new Auth();
t.type(Auth, 'function');
t.type(a, 'object');
t.type(a.unsign, 'function');
t.end();
});
test('valid', function (t) {
const c = cases.valid;
const a = new Auth(c.salt, c.secret);
t.strictEqual(a.unsign(c.signed), c.unsigned);
t.end();
});
test('invalid salt', function (t) {
const c = cases.invalidSalt;
const a = new Auth(c.salt, c.secret);
t.strictEqual(a.unsign(c.signed), c.unsigned);
t.end();
});
test('invalid secret', function (t) {
const c = cases.invalidSecret;
const a = new Auth(c.salt, c.secret);
t.strictEqual(a.unsign(c.signed), c.unsigned);
t.end();
});
test('invalid token', function (t) {
const c = cases.invalidToken;
const a = new Auth(c.salt, c.secret);
t.strictEqual(a.unsign(c.signed), c.unsigned);
t.end();
});
test('missing token', function (t) {
const c = cases.missingToken;
const a = new Auth(c.salt, c.secret);
t.strictEqual(a.unsign(c.signed), c.unsigned);
t.end();
});

45
test/unit/signer.js Normal file
View file

@ -0,0 +1,45 @@
const test = require('tap').test;
const signer = require('../../lib/signer');
test('spec', function (t) {
t.type(signer, 'object');
t.type(signer.getSaltedHmacKey, 'function');
t.type(signer.getSaltedHmac, 'function');
t.type(signer.base64Hmac, 'function');
t.end();
});
test('getSaltedHmacKey', function (t) {
// Valid input
t.strictEqual(
signer.getSaltedHmacKey('foo', 'bar'),
'\x88C×ù$\x16!\x1Déë¹cÿLâ\x81%\x93(x'
);
// Invalid input
t.strictEqual(signer.getSaltedHmacKey('foo', undefined), undefined);
t.strictEqual(signer.getSaltedHmacKey(undefined, 'bar'), undefined);
t.end();
});
test('getSaltedHmac', function (t) {
// Valid input
t.type(signer.getSaltedHmac('foo', 'bar', 'baz'), 'object');
// Invalid input
t.strictEqual(signer.getSaltedHmac('foo', 'bar', undefined), undefined);
t.strictEqual(signer.getSaltedHmac('foo', undefined, 'baz'), undefined);
t.strictEqual(signer.getSaltedHmac(undefined, 'bar', 'baz'), undefined);
t.end();
});
test('base64Hmac', function (t) {
// Valid input
t.type(signer.base64Hmac('foo', 'bar', 'baz'), 'string');
// Invalid input
t.strictEqual(signer.base64Hmac('foo', 'bar', undefined), undefined);
t.strictEqual(signer.base64Hmac('foo', undefined, 'baz'), undefined);
t.strictEqual(signer.base64Hmac(undefined, 'bar', 'baz'), undefined);
t.end();
});

53
test/unit/util.js Normal file
View file

@ -0,0 +1,53 @@
const test = require('tap').test;
const util = require('../../lib/util');
test('spec', function (t) {
t.type(util, 'object');
t.type(util.b64Encode, 'function');
t.type(util.b64Decode, 'function');
t.type(util.md5, 'function');
t.end();
});
test('b64Encode', function (t) {
// Valid input
t.strictEqual(util.b64Encode('foobar'), 'Zm9vYmFy');
t.strictEqual(util.b64Encode('http://foo-bar'), 'aHR0cDovL2Zvby1iYXI');
t.strictEqual(util.b64Encode('http:/`\\'), 'aHR0cDovYFw');
// Invalid input
t.strictEqual(util.b64Encode(undefined), undefined);
t.strictEqual(util.b64Encode(null), undefined);
t.strictEqual(util.b64Encode(NaN), undefined);
t.strictEqual(util.b64Encode(0), undefined);
t.strictEqual(util.b64Encode({}), undefined);
t.end();
});
test('b64Decode', function (t) {
// Valid input
t.strictEqual(util.b64Decode('Zm9vYmFy'), 'foobar');
t.strictEqual(util.b64Decode('aHR0cDovL2Zvby1iYXI'), 'http://foo-bar');
t.strictEqual(util.b64Decode('.aHR0cDovYFy='), 'http:/`\\');
// Invalid input
t.strictEqual(util.b64Decode(undefined), undefined);
t.strictEqual(util.b64Decode(null), undefined);
t.strictEqual(util.b64Decode(NaN), undefined);
t.strictEqual(util.b64Decode(0), undefined);
t.strictEqual(util.b64Decode({}), undefined);
t.end();
});
test('md5', function (t) {
// Valid input
t.strictEqual(util.md5('foobar'), '3858f62230ac3c915f300c664312c63f');
// Invalid input
t.strictEqual(util.md5(undefined), undefined);
t.strictEqual(util.md5(null), undefined);
t.strictEqual(util.md5(NaN), undefined);
t.strictEqual(util.md5(0), undefined);
t.strictEqual(util.md5({}), undefined);
t.end();
});