Merge remote-tracking branch 'origin/signing' into v1.0
This commit is contained in:
		
						commit
						5ed05f03cf
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | node_modules/ | ||||||
							
								
								
									
										10
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								README.md
									
									
									
									
									
								
							| @ -6,7 +6,7 @@ The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript im | |||||||
| 
 | 
 | ||||||
| Instead of bloating your webapp and ruining the mobile experience, | Instead of bloating your webapp and ruining the mobile experience, | ||||||
| you can use a single, small javascript file for all OAuth3 providers | you can use a single, small javascript file for all OAuth3 providers | ||||||
| (and almost all OAuth2 providers) with a seemless experience. | (and almost all OAuth2 providers) with a seamless experience. | ||||||
| 
 | 
 | ||||||
| Also, instead of complicated (or worse - insecure) CLI and Desktop login methods, | Also, instead of complicated (or worse - insecure) CLI and Desktop login methods, | ||||||
| you can easily integrate an OAuth3 flow (or broker) into any node.js app (i.e. Electron, Node-Webkit) | you can easily integrate an OAuth3 flow (or broker) into any node.js app (i.e. Electron, Node-Webkit) | ||||||
| @ -74,7 +74,7 @@ function onClickLogin() { | |||||||
|     console.info('Authentication was Successful:'); |     console.info('Authentication was Successful:'); | ||||||
|     console.log(session); |     console.log(session); | ||||||
| 
 | 
 | ||||||
|     // You can use the PPID (or preferrably a hash of it) as the login for your app |     // You can use the PPID (or preferably a hash of it) as the login for your app | ||||||
|     // (it securely functions as both username and password which is known only by your app) |     // (it securely functions as both username and password which is known only by your app) | ||||||
|     // If you use a hash of it as an ID, you can also use the PPID itself as a decryption key |     // If you use a hash of it as an ID, you can also use the PPID itself as a decryption key | ||||||
|     // |     // | ||||||
| @ -168,7 +168,7 @@ pushd /path/to/your/web/app | |||||||
| # clone the project as assets/org.oauth3 | # clone the project as assets/org.oauth3 | ||||||
| mkdir -p assets | mkdir -p assets | ||||||
| git clone git@git.daplie.com:Daplie/oauth3.js.git assets/org.oauth3 | git clone git@git.daplie.com:Daplie/oauth3.js.git assets/org.oauth3 | ||||||
| pushd assests/org.oauth3 | pushd assets/org.oauth3 | ||||||
| git checkout v1 | git checkout v1 | ||||||
| popd | popd | ||||||
| 
 | 
 | ||||||
| @ -232,7 +232,7 @@ function onClickLogin() { | |||||||
|     console.info('Authentication was Successful:'); |     console.info('Authentication was Successful:'); | ||||||
|     console.log(session); |     console.log(session); | ||||||
| 
 | 
 | ||||||
|     // You can use the PPID (or preferrably a hash of it) as the login for your app |     // You can use the PPID (or preferably a hash of it) as the login for your app | ||||||
|     // (it securely functions as both username and password which is known only by your app) |     // (it securely functions as both username and password which is known only by your app) | ||||||
|     // If you use a hash of it as an ID, you can also use the PPID itself as a decryption key |     // If you use a hash of it as an ID, you can also use the PPID itself as a decryption key | ||||||
|     // |     // | ||||||
| @ -448,7 +448,7 @@ As a general rule I don't like rules that sometimes apply and sometimes don't, | |||||||
| so I may need to rethink this. However, there are cases where including the protocol | so I may need to rethink this. However, there are cases where including the protocol | ||||||
| can be very ugly and confusing and we definitely need to allow relative paths. | can be very ugly and confusing and we definitely need to allow relative paths. | ||||||
| 
 | 
 | ||||||
| A potential work-around would be to assume all paths are relative (elimitate #4 instead) | A potential work-around would be to assume all paths are relative (eliminate #4 instead) | ||||||
| and have the path always key off of the base URL - if oauth3 directives are to be found at | and have the path always key off of the base URL - if oauth3 directives are to be found at | ||||||
| https://example.com/username/.well-known/oauth3/directives.json then /api/whatever would refer | https://example.com/username/.well-known/oauth3/directives.json then /api/whatever would refer | ||||||
| to https://example.com/username/api/whatever. | to https://example.com/username/api/whatever. | ||||||
|  | |||||||
							
								
								
									
										97
									
								
								browserify/crypto.fallback.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								browserify/crypto.fallback.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,97 @@ | |||||||
|  | ;(function () { | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  |   var createHash = require('create-hash'); | ||||||
|  |   var pbkdf2 = require('pbkdf2'); | ||||||
|  |   var aes = require('browserify-aes'); | ||||||
|  |   var ec = require('elliptic/lib/elliptic/ec')('p256'); | ||||||
|  | 
 | ||||||
|  |   function sha256(buf) { | ||||||
|  |     return createHash('sha256').update(buf).digest(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function runPbkdf2(password, salt) { | ||||||
|  |     // Derived AES key is 128 bit, and the function takes a size in bytes.
 | ||||||
|  |     return pbkdf2.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256'); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function encrypt(key, iv, data) { | ||||||
|  |     var cipher = aes.createCipheriv('aes-128-gcm', Buffer(key), Buffer(iv)); | ||||||
|  | 
 | ||||||
|  |     return Buffer.concat([cipher.update(Buffer(data)), cipher.final(), cipher.getAuthTag()]); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function decrypt(key, iv, data) { | ||||||
|  |     var decipher = aes.createDecipheriv('aes-128-gcm', Buffer(key), Buffer(iv)); | ||||||
|  | 
 | ||||||
|  |     decipher.setAuthTag(Buffer(data.slice(-16))); | ||||||
|  |     return Buffer.concat([decipher.update(Buffer(data.slice(0, -16))), decipher.final()]); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function bnToBuffer(bn, size) { | ||||||
|  |     var buf = bn.toArrayLike(Buffer); | ||||||
|  | 
 | ||||||
|  |     if (!size || buf.length === size) { | ||||||
|  |       return buf; | ||||||
|  |     } else if (buf.length < size) { | ||||||
|  |       return Buffer.concat([Buffer(size-buf.length).fill(0), buf]); | ||||||
|  |     } else if (buf.length > size) { | ||||||
|  |       throw new Error('EC signature number bigger than expected'); | ||||||
|  |     } | ||||||
|  |     throw new Error('invalid size "'+size+'" converting BigNumber to Buffer'); | ||||||
|  |   } | ||||||
|  |   function bnToB64(bn) { | ||||||
|  |     var b64 = bnToBuffer(bn).toString('base64'); | ||||||
|  |     return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/, ''); | ||||||
|  |   } | ||||||
|  |   function genEcdsaKeyPair() { | ||||||
|  |     var key = ec.genKeyPair(); | ||||||
|  |     var pubJwk = { | ||||||
|  |       key_ops: ['verify'] | ||||||
|  |     , kty: 'EC' | ||||||
|  |     , crv: 'P-256' | ||||||
|  |     , x: bnToB64(key.getPublic().getX()) | ||||||
|  |     , y: bnToB64(key.getPublic().getY()) | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     var privJwk = JSON.parse(JSON.stringify(pubJwk)); | ||||||
|  |     privJwk.key_ops = ['sign']; | ||||||
|  |     privJwk.d = bnToB64(key.getPrivate()); | ||||||
|  | 
 | ||||||
|  |     return {privateKey: privJwk, publicKey: pubJwk}; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function sign(jwk, msg) { | ||||||
|  |     var key = ec.keyFromPrivate(Buffer(jwk.d, 'base64')); | ||||||
|  |     var sig = key.sign(sha256(msg)); | ||||||
|  |     return Buffer.concat([bnToBuffer(sig.r, 32), bnToBuffer(sig.s, 32)]); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function verify(jwk, msg, signature) { | ||||||
|  |     var key = ec.keyFromPublic({x: Buffer(jwk.x, 'base64'), y: Buffer(jwk.y, 'base64')}); | ||||||
|  |     var sig = { | ||||||
|  |       r: Buffer(signature.slice(0, signature.length/2)) | ||||||
|  |     , s: Buffer(signature.slice(signature.length/2)) | ||||||
|  |     }; | ||||||
|  |     return key.verify(sha256(msg), sig); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function promiseWrap(func) { | ||||||
|  |     return function() { | ||||||
|  |       var args = arguments; | ||||||
|  |       // This fallback file should only be used when the browser doesn't support everything we
 | ||||||
|  |       // need with WebCrypto. Since it is only used in the browser we should be able to assume
 | ||||||
|  |       // that OAUTH3 has been placed in the global scope and that we can access it here.
 | ||||||
|  |       return new OAUTH3.PromiseA(function (resolve) { | ||||||
|  |         resolve(func.apply(null, args)); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |   exports.sha256  = promiseWrap(sha256); | ||||||
|  |   exports.pbkdf2  = promiseWrap(runPbkdf2); | ||||||
|  |   exports.encrypt = promiseWrap(encrypt); | ||||||
|  |   exports.decrypt = promiseWrap(decrypt); | ||||||
|  |   exports.sign    = promiseWrap(sign); | ||||||
|  |   exports.verify  = promiseWrap(verify); | ||||||
|  |   exports.genEcdsaKeyPair = promiseWrap(genEcdsaKeyPair); | ||||||
|  | }()); | ||||||
							
								
								
									
										25
									
								
								gulpfile.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								gulpfile.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | |||||||
|  | ;(function () { | ||||||
|  |   'use strict'; | ||||||
|  | 
 | ||||||
|  |   var gulp = require('gulp'); | ||||||
|  |   var browserify = require('browserify'); | ||||||
|  |   var source = require('vinyl-source-stream'); | ||||||
|  |   var streamify = require('gulp-streamify'); | ||||||
|  |   var uglify = require('gulp-uglify'); | ||||||
|  |   var rename = require('gulp-rename'); | ||||||
|  | 
 | ||||||
|  |   gulp.task('default', function () { | ||||||
|  |     return browserify('./browserify/crypto.fallback.js', {standalone: 'OAUTH3_crypto_fallback'}).bundle() | ||||||
|  |       .pipe(source('browserify/crypto.fallback.js')) | ||||||
|  |       .pipe(rename('oauth3.crypto.fallback.js')) | ||||||
|  |       .pipe(gulp.dest('./')) | ||||||
|  |       .pipe(streamify(uglify())) | ||||||
|  |       .pipe(rename('oauth3.crypto.fallback.min.js')) | ||||||
|  |       .pipe(gulp.dest('./')) | ||||||
|  |       ; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   gulp.task('watch', function () { | ||||||
|  |     gulp.watch('browserify/*.js', [ 'default' ]); | ||||||
|  |   }); | ||||||
|  | }()); | ||||||
| @ -14,6 +14,27 @@ | |||||||
|         return err; |         return err; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |   , _binStr: { | ||||||
|  |       bufferToBinStr: function (buf) { | ||||||
|  |         return Array.prototype.map.call(new Uint8Array(buf), function(ch) { | ||||||
|  |           return String.fromCharCode(ch); | ||||||
|  |         }).join(''); | ||||||
|  |       } | ||||||
|  |     , binStrToBuffer: function (str) { | ||||||
|  |         var buf; | ||||||
|  | 
 | ||||||
|  |         if ('undefined' !== typeof Uint8Array) { | ||||||
|  |           buf = new Uint8Array(str.length); | ||||||
|  |         } else { | ||||||
|  |           buf = []; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Array.prototype.forEach.call(str, function (ch, ind) { | ||||||
|  |           buf[ind] = ch.charCodeAt(0); | ||||||
|  |         }); | ||||||
|  |         return buf; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   , _base64: { |   , _base64: { | ||||||
|       atob: function (base64) { |       atob: function (base64) { | ||||||
|         // atob must be called from the global context
 |         // atob must be called from the global context
 | ||||||
| @ -43,6 +64,12 @@ | |||||||
|         b64 = b64.replace(/=+/g, ''); |         b64 = b64.replace(/=+/g, ''); | ||||||
|         return b64; |         return b64; | ||||||
|       } |       } | ||||||
|  |     , urlSafeToBuffer: function (str) { | ||||||
|  |         return OAUTH3._binStr.binStrToBuffer(OAUTH3._base64.decodeUrlSafe(str)); | ||||||
|  |       } | ||||||
|  |     , bufferToUrlSafe: function (buf) { | ||||||
|  |         return OAUTH3._base64.encodeUrlSafe(OAUTH3._binStr.bufferToBinStr(buf)); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   , uri: { |   , uri: { | ||||||
|       normalize: function (uri) { |       normalize: function (uri) { | ||||||
| @ -181,15 +208,17 @@ | |||||||
|         // { header: {}, payload: {}, signature: '' }
 |         // { header: {}, payload: {}, signature: '' }
 | ||||||
|         var parts = str.split(/\./g); |         var parts = str.split(/\./g); | ||||||
|         var jsons = parts.slice(0, 2).map(function (urlsafe64) { |         var jsons = parts.slice(0, 2).map(function (urlsafe64) { | ||||||
|           var b64 = OAUTH3._base64.decodeUrlSafe(urlsafe64); |           return JSON.parse(OAUTH3._base64.decodeUrlSafe(urlsafe64)); | ||||||
|           return b64; |  | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         return { |         return { header: jsons[0], payload: jsons[1] }; | ||||||
|           header: JSON.parse(jsons[0]) |       } | ||||||
|         , payload: JSON.parse(jsons[1]) |     , verify: function (jwk, token) { | ||||||
|         , signature: parts[2] // should remain url-safe base64
 |         var parts = token.split(/\./g); | ||||||
|         }; |         var data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.')); | ||||||
|  |         var signature = OAUTH3._base64.urlSafeToBuffer(parts[2]); | ||||||
|  | 
 | ||||||
|  |         return OAUTH3.crypto.core.verify(jwk, data, signature); | ||||||
|       } |       } | ||||||
|     , freshness: function (tokenMeta, staletime, _now) { |     , freshness: function (tokenMeta, staletime, _now) { | ||||||
|         staletime = staletime || (15 * 60); |         staletime = staletime || (15 * 60); | ||||||
| @ -740,6 +769,7 @@ | |||||||
|           return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params)); |           return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params)); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         OAUTH3.hooks.session._cache = {}; | ||||||
|         return params; |         return params; | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  | |||||||
							
								
								
									
										293
									
								
								oauth3.crypto.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										293
									
								
								oauth3.crypto.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,293 @@ | |||||||
|  | ;(function (exports) { | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  |   var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; | ||||||
|  | 
 | ||||||
|  |   OAUTH3.crypto = {}; | ||||||
|  |   try { | ||||||
|  |     OAUTH3.crypto.core = require('./oauth3.node.crypto'); | ||||||
|  |   } catch (error) { | ||||||
|  |     OAUTH3.crypto.core = {}; | ||||||
|  | 
 | ||||||
|  |     // We don't currently have a fallback method for this function, so we assign
 | ||||||
|  |     // it directly to the core object instead of the webCrypto object.
 | ||||||
|  |     OAUTH3.crypto.core.randomBytes = function (size) { | ||||||
|  |       var buf = OAUTH3._browser.window.crypto.getRandomValues(new Uint8Array(size)); | ||||||
|  |       return OAUTH3.PromiseA.resolve(buf); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     var webCrypto = {}; | ||||||
|  |     webCrypto.sha256 = function (buf) { | ||||||
|  |       return OAUTH3._browser.window.crypto.subtle.digest({name: 'SHA-256'}, buf); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     webCrypto.pbkdf2 = function (password, salt) { | ||||||
|  |       return OAUTH3._browser.window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(password), {name: 'PBKDF2'}, false, ['deriveKey']) | ||||||
|  |         .then(function (key) { | ||||||
|  |           var opts = {name: 'PBKDF2', salt: salt, iterations: 8192, hash: {name: 'SHA-256'}}; | ||||||
|  |           return OAUTH3._browser.window.crypto.subtle.deriveKey(opts, key, {name: 'AES-GCM', length: 128}, true, ['encrypt', 'decrypt']); | ||||||
|  |         }) | ||||||
|  |         .then(function (key) { | ||||||
|  |           return OAUTH3._browser.window.crypto.subtle.exportKey('raw', key); | ||||||
|  |         }); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     webCrypto.encrypt = function (rawKey, iv, data) { | ||||||
|  |       return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['encrypt']) | ||||||
|  |         .then(function (key) { | ||||||
|  |           return OAUTH3._browser.window.crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, key, data); | ||||||
|  |         }); | ||||||
|  |     }; | ||||||
|  |     webCrypto.decrypt = function (rawKey, iv, data) { | ||||||
|  |       return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['decrypt']) | ||||||
|  |         .then(function (key) { | ||||||
|  |           return OAUTH3._browser.window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, data); | ||||||
|  |         }); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     webCrypto.genEcdsaKeyPair = function () { | ||||||
|  |       return OAUTH3._browser.window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify']) | ||||||
|  |         .then(function (keyPair) { | ||||||
|  |           return OAUTH3.PromiseA.all([ | ||||||
|  |             OAUTH3._browser.window.crypto.subtle.exportKey('jwk', keyPair.privateKey) | ||||||
|  |           , OAUTH3._browser.window.crypto.subtle.exportKey('jwk', keyPair.publicKey) | ||||||
|  |           ]); | ||||||
|  |         }).then(function (jwkPair) { | ||||||
|  |           return { privateKey: jwkPair[0], publicKey:  jwkPair[1] }; | ||||||
|  |         }); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     webCrypto.sign = function (jwk, msg) { | ||||||
|  |       return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign']) | ||||||
|  |         .then(function (key) { | ||||||
|  |           return OAUTH3._browser.window.crypto.subtle.sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, msg); | ||||||
|  |         }) | ||||||
|  |         .then(function (sig) { | ||||||
|  |           return new Uint8Array(sig); | ||||||
|  |         }); | ||||||
|  |     }; | ||||||
|  |     webCrypto.verify = function (jwk, msg, signature) { | ||||||
|  |       // If the JWK has properties that should only exist on the private key or is missing
 | ||||||
|  |       // "verify" in the key_ops, importing in as a public key won't work.
 | ||||||
|  |       if (jwk.hasOwnProperty('d') || jwk.hasOwnProperty('key_ops')) { | ||||||
|  |         jwk = JSON.parse(JSON.stringify(jwk)); | ||||||
|  |         delete jwk.d; | ||||||
|  |         delete jwk.key_ops; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['verify']) | ||||||
|  |       .then(function (key) { | ||||||
|  |         return OAUTH3._browser.window.crypto.subtle.verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, signature, msg); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     function checkWebCrypto() { | ||||||
|  |       var loadFallback = function() { | ||||||
|  |         var prom; | ||||||
|  |         loadFallback = function () { return prom; }; | ||||||
|  | 
 | ||||||
|  |         prom = new OAUTH3.PromiseA(function (resolve) { | ||||||
|  |           var body = document.getElementsByTagName('body')[0]; | ||||||
|  |           var script = document.createElement('script'); | ||||||
|  |           script.type = 'text/javascript'; | ||||||
|  |           script.onload = resolve; | ||||||
|  |           script.onreadystatechange = function () { | ||||||
|  |             if (this.readyState === 'complete' || this.readyState === 'loaded') { | ||||||
|  |               resolve(); | ||||||
|  |             } | ||||||
|  |           }; | ||||||
|  |           script.src = '/assets/org.oauth3/oauth3.crypto.fallback.js'; | ||||||
|  |           body.appendChild(script); | ||||||
|  |         }); | ||||||
|  |         return prom; | ||||||
|  |       }; | ||||||
|  |       function checkException(name, func) { | ||||||
|  |         new OAUTH3.PromiseA(function (resolve) { resolve(func()); }) | ||||||
|  |           .then(function () { | ||||||
|  |             OAUTH3.crypto.core[name] = webCrypto[name]; | ||||||
|  |           }) | ||||||
|  |           .catch(function (err) { | ||||||
|  |             console.warn('error with WebCrypto', name, '- using fallback', err); | ||||||
|  |             loadFallback().then(function () { | ||||||
|  |               OAUTH3.crypto.core[name] = OAUTH3_crypto_fallback[name]; | ||||||
|  |             }); | ||||||
|  |           }); | ||||||
|  |       } | ||||||
|  |       function checkResult(name, expected, func) { | ||||||
|  |         checkException(name, function () { | ||||||
|  |           return func() | ||||||
|  |             .then(function (result) { | ||||||
|  |               if (typeof expected === typeof result) { | ||||||
|  |                 return result; | ||||||
|  |               } | ||||||
|  |               return OAUTH3._base64.bufferToUrlSafe(result); | ||||||
|  |             }) | ||||||
|  |             .then(function (result) { | ||||||
|  |               if (result !== expected) { | ||||||
|  |                 throw new Error("result ("+result+") doesn't match expectation ("+expected+")"); | ||||||
|  |               } | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       var zeroBuf = new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); | ||||||
|  |       var dataBuf = OAUTH3._base64.urlSafeToBuffer('1234567890abcdefghijklmn'); | ||||||
|  |       var keyBuf = OAUTH3._base64.urlSafeToBuffer('l_Aeoqk6ePjwjCYrlHrgrg'); | ||||||
|  |       var encBuf = OAUTH3._base64.urlSafeToBuffer('Ji_gEtcNElUONSR4Mf9S75davXjh_6-oQN9AgO5UF8rERw'); | ||||||
|  |       checkResult('sha256', 'BwMveUm2V1axuERvUoxM4dScgNl9yKhER9a6p80GXj4', function () { | ||||||
|  |         return webCrypto.sha256(dataBuf); | ||||||
|  |       }); | ||||||
|  |       checkResult('pbkdf2', OAUTH3._base64.bufferToUrlSafe(keyBuf), function () { | ||||||
|  |         return webCrypto.pbkdf2('password', zeroBuf); | ||||||
|  |       }); | ||||||
|  |       checkResult('encrypt', OAUTH3._base64.bufferToUrlSafe(encBuf), function () { | ||||||
|  |         return webCrypto.encrypt(keyBuf, zeroBuf.slice(0, 12), dataBuf); | ||||||
|  |       }); | ||||||
|  |       checkResult('decrypt', OAUTH3._base64.bufferToUrlSafe(dataBuf), function () { | ||||||
|  |         return webCrypto.decrypt(keyBuf, zeroBuf.slice(0, 12), encBuf); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       var jwk = { | ||||||
|  |         kty: "EC" | ||||||
|  |       , crv: "P-256" | ||||||
|  |       , d: "ChXx7ea5YtEltCufA8CVb0lQv3glcCfcSpEgdedgIP0" | ||||||
|  |       , x: "Akt5ZDbytcKS5UQMURvGb_UIMS4qFctDwrX8bX22ato" | ||||||
|  |       , y: "cV7nhpWNT1FeRIbdold4jLtgsEpZBFcNy3p2E5mqvto" | ||||||
|  |       }; | ||||||
|  |       var sig = OAUTH3._base64.urlSafeToBuffer('nc3F8qeP8OXpfqPD9tTcFQg0Wfp37RTAppLPIKE1ZupR_8Aba64hNExwd1dOk802OFQxaECPDZCkKe7WA9RXAg'); | ||||||
|  |       checkResult('verify', true, function() { | ||||||
|  |         return webCrypto.verify(jwk, dataBuf, sig); | ||||||
|  |       }); | ||||||
|  |       // The results of these functions are less predictable, so we can't check their return value.
 | ||||||
|  |       checkException('genEcdsaKeyPair', function () { | ||||||
|  |         return webCrypto.genEcdsaKeyPair(); | ||||||
|  |       }); | ||||||
|  |       checkException('sign', function () { | ||||||
|  |         return webCrypto.sign(jwk, dataBuf); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     checkWebCrypto(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   OAUTH3.crypto.thumbprintJwk = function (jwk) { | ||||||
|  |     var keys; | ||||||
|  |     if (jwk.kty === 'EC') { | ||||||
|  |       keys = ['crv', 'x', 'y']; | ||||||
|  |     } else if (jwk.kty === 'RSA') { | ||||||
|  |       keys = ['e', 'n']; | ||||||
|  |     } else if (jwk.kty === 'oct') { | ||||||
|  |       keys = ['k']; | ||||||
|  |     } else { | ||||||
|  |       return OAUTH3.PromiseA.reject(new Error('invalid JWK key type ' + jwk.kty)); | ||||||
|  |     } | ||||||
|  |     keys.push('kty'); | ||||||
|  |     keys.sort(); | ||||||
|  | 
 | ||||||
|  |     var missing = keys.filter(function (name) { return !jwk.hasOwnProperty(name); }); | ||||||
|  |     if (missing.length > 0) { | ||||||
|  |       return OAUTH3.PromiseA.reject(new Error('JWK of type '+jwk.kty+' missing fields ' + missing)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var jwkStr = '{' + keys.map(function (name) { return name+':'+jwk[name]; }).join(',') + '}'; | ||||||
|  |     return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(jwkStr)) | ||||||
|  |       .then(OAUTH3._base64.bufferToUrlSafe); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   OAUTH3.crypto._createKey = function (ppid) { | ||||||
|  |     var saltProm = OAUTH3.crypto.core.randomBytes(16); | ||||||
|  |     var kekProm = saltProm.then(function (salt) { | ||||||
|  |       return OAUTH3.crypto.core.pbkdf2(ppid, salt); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     var ecdsaProm = OAUTH3.crypto.core.genEcdsaKeyPair() | ||||||
|  |     .then(function (keyPair) { | ||||||
|  |       return OAUTH3.crypto.thumbprintJwk(keyPair.publicKey).then(function (kid) { | ||||||
|  |         keyPair.privateKey.alg = keyPair.publicKey.alg = 'ES256'; | ||||||
|  |         keyPair.privateKey.kid = keyPair.publicKey.kid = kid; | ||||||
|  |         return keyPair; | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return OAUTH3.PromiseA.all([ | ||||||
|  |       kekProm | ||||||
|  |     , ecdsaProm | ||||||
|  |     , saltProm | ||||||
|  |     , OAUTH3.crypto.core.randomBytes(16) | ||||||
|  |     , OAUTH3.crypto.core.randomBytes(12) | ||||||
|  |     , OAUTH3.crypto.core.randomBytes(12) | ||||||
|  |     ]).then(function (results) { | ||||||
|  |       var kek        = results[0]; | ||||||
|  |       var keyPair    = results[1]; | ||||||
|  |       var salt       = results[2]; | ||||||
|  |       var userSecret = results[3]; | ||||||
|  |       var ecdsaIv    = results[4]; | ||||||
|  |       var secretIv   = results[5]; | ||||||
|  | 
 | ||||||
|  |       return OAUTH3.PromiseA.all([ | ||||||
|  |         OAUTH3.crypto.core.encrypt(kek, ecdsaIv, OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey))) | ||||||
|  |       , OAUTH3.crypto.core.encrypt(kek, secretIv, userSecret) | ||||||
|  |       ]) | ||||||
|  |       .then(function (encrypted) { | ||||||
|  |         return { | ||||||
|  |           publicKey:  keyPair.publicKey | ||||||
|  |         , privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted[0]) | ||||||
|  |         , userSecret: OAUTH3._base64.bufferToUrlSafe(encrypted[1]) | ||||||
|  |         , salt:       OAUTH3._base64.bufferToUrlSafe(salt) | ||||||
|  |         , ecdsaIv:    OAUTH3._base64.bufferToUrlSafe(ecdsaIv) | ||||||
|  |         , secretIv:   OAUTH3._base64.bufferToUrlSafe(secretIv) | ||||||
|  |         }; | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   OAUTH3.crypto._decryptKey = function (ppid, storedObj) { | ||||||
|  |     var salt   = OAUTH3._base64.urlSafeToBuffer(storedObj.salt); | ||||||
|  |     var encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey); | ||||||
|  |     var iv     = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv); | ||||||
|  | 
 | ||||||
|  |     return OAUTH3.crypto.core.pbkdf2(ppid, salt) | ||||||
|  |       .then(function (key) { | ||||||
|  |         return OAUTH3.crypto.core.decrypt(key, iv, encJwk); | ||||||
|  |       }) | ||||||
|  |       .then(OAUTH3._binStr.bufferToBinStr) | ||||||
|  |       .then(JSON.parse); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   OAUTH3.crypto._getKey = function (ppid) { | ||||||
|  |     return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(ppid)) | ||||||
|  |     .then(function (hash) { | ||||||
|  |       var name = 'kek-' + OAUTH3._base64.bufferToUrlSafe(hash); | ||||||
|  |       var promise; | ||||||
|  | 
 | ||||||
|  |       if (window.localStorage.getItem(name) === null) { | ||||||
|  |         promise = OAUTH3.crypto._createKey(ppid).then(function (key) { | ||||||
|  |           window.localStorage.setItem(name, JSON.stringify(key)); | ||||||
|  |           return key; | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         promise = OAUTH3.PromiseA.resolve(JSON.parse(window.localStorage.getItem(name))); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return promise.then(function (storedObj) { | ||||||
|  |         return OAUTH3.crypto._decryptKey(ppid, storedObj); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   OAUTH3.crypto._signPayload = function (payload) { | ||||||
|  |     return OAUTH3.crypto._getKey('some PPID').then(function (key) { | ||||||
|  |       var header = {type: 'JWT', alg: key.alg, kid: key.kid}; | ||||||
|  |       var input = [ | ||||||
|  |         OAUTH3._base64.encodeUrlSafe(JSON.stringify(header, null)) | ||||||
|  |       , OAUTH3._base64.encodeUrlSafe(JSON.stringify(payload, null)) | ||||||
|  |       ].join('.'); | ||||||
|  | 
 | ||||||
|  |       return OAUTH3.crypto.core.sign(key, OAUTH3._binStr.binStrToBuffer(input)) | ||||||
|  |         .then(OAUTH3._base64.bufferToUrlSafe) | ||||||
|  |         .then(function (signature) { | ||||||
|  |           return input + '.' + signature; | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  | }('undefined' !== typeof exports ? exports : window)); | ||||||
| @ -3,30 +3,6 @@ | |||||||
| 
 | 
 | ||||||
|   var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; |   var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; | ||||||
| 
 | 
 | ||||||
|   OAUTH3._base64.btoa = function (b64) { |  | ||||||
|     // http://stackoverflow.com/questions/9677985/uncaught-typeerror-illegal-invocation-in-chrome
 |  | ||||||
|     return (exports.btoa || require('btoa'))(b64); |  | ||||||
|   }; |  | ||||||
|   OAUTH3._base64.encodeUrlSafe = function (b64) { |  | ||||||
|     // Base64 to URL-safe Base64
 |  | ||||||
|     b64 = b64.replace(/\+/g, '-').replace(/\//g, '_'); |  | ||||||
|     b64 = b64.replace(/=+/g, ''); |  | ||||||
|     return OAUTH3._base64.btoa(b64); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   OAUTH3.jwt.encode = function (parts) { |  | ||||||
|     parts.header = parts.header || { alg: 'none', typ: 'jwt' }; |  | ||||||
|     parts.signature = parts.signature || ''; |  | ||||||
| 
 |  | ||||||
|     var result = [ |  | ||||||
|       OAUTH3._base64.encodeUrlSafe(JSON.stringify(parts.header, null)) |  | ||||||
|     , OAUTH3._base64.encodeUrlSafe(JSON.stringify(parts.payload, null)) |  | ||||||
|     , parts.signature // should already be url-safe base64
 |  | ||||||
|     ].join('.'); |  | ||||||
| 
 |  | ||||||
|     return result; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   OAUTH3.authn.resourceOwnerPassword = OAUTH3.authz.resourceOwnerPassword = function (directive, opts) { |   OAUTH3.authn.resourceOwnerPassword = OAUTH3.authz.resourceOwnerPassword = function (directive, opts) { | ||||||
|     var providerUri = directive.issuer; |     var providerUri = directive.issuer; | ||||||
| 
 | 
 | ||||||
| @ -50,12 +26,12 @@ | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   OAUTH3.authz.scopes = function () { |   OAUTH3.authz.scopes = function () { | ||||||
|     return { |     return OAUTH3.PromiseA.resolve({ | ||||||
|       pending: ['oauth3_authn']   // not yet accepted
 |       pending: ['oauth3_authn']   // not yet accepted
 | ||||||
|     , granted: []                 // all granted, ever
 |     , granted: []                 // all granted, ever
 | ||||||
|     , requested: ['oauth3_authn'] // all requested, now
 |     , requested: ['oauth3_authn'] // all requested, now
 | ||||||
|     , accepted: []                // granted (ever) and requested (now)
 |     , accepted: []                // granted (ever) and requested (now)
 | ||||||
|     }; |     }); | ||||||
|   }; |   }; | ||||||
|   OAUTH3.authz.grants = function (providerUri, opts) { |   OAUTH3.authz.grants = function (providerUri, opts) { | ||||||
|     if ('POST' === opts.method) { |     if ('POST' === opts.method) { | ||||||
| @ -82,24 +58,21 @@ | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   OAUTH3._mockToken = function (providerUri, opts) { |   OAUTH3._mockToken = function (providerUri, opts) { | ||||||
|     var accessToken = OAUTH3.jwt.encode({ |     var payload = { exp: Math.round(Date.now() / 1000) + 900, sub: 'fakeUserId', scp: opts.scope }; | ||||||
|       header: { alg: 'none' } |     return OAUTH3.crypto._signPayload(payload).then(function (accessToken) { | ||||||
|     , payload: { exp: Math.round(Date.now() / 1000) + 900, sub: 'fakeUserId', scp: opts.scope } |       return OAUTH3.hooks.session.refresh( | ||||||
|     , signature: "fakeSig" |         opts.session || { | ||||||
|  |           provider_uri: providerUri | ||||||
|  |         , client_id: opts.client_id | ||||||
|  |         , client_uri: opts.client_uri || opts.clientUri | ||||||
|  |         } | ||||||
|  |       , { access_token: accessToken | ||||||
|  |         , refresh_token: accessToken | ||||||
|  |         , expires_in: "900" | ||||||
|  |         , scope: opts.scope | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|     }); |     }); | ||||||
| 
 |  | ||||||
|     return OAUTH3.hooks.session.refresh( |  | ||||||
|       opts.session || { |  | ||||||
|         provider_uri: providerUri |  | ||||||
|       , client_id: opts.client_id |  | ||||||
|       , client_uri: opts.client_uri || opts.clientUri |  | ||||||
|       } |  | ||||||
|     , { access_token: accessToken |  | ||||||
|       , refresh_token: accessToken |  | ||||||
|       , expires_in: "900" |  | ||||||
|       , scope: opts.scope |  | ||||||
|       } |  | ||||||
|     ); |  | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
| }('undefined' !== typeof exports ? exports : window)); | }('undefined' !== typeof exports ? exports : window)); | ||||||
|  | |||||||
							
								
								
									
										106
									
								
								oauth3.node.crypto.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								oauth3.node.crypto.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,106 @@ | |||||||
|  | ;(function () { | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  |   var crypto = require('crypto'); | ||||||
|  |   var OAUTH3 = require('./oauth3.core.js').OAUTH3; | ||||||
|  |   var ec = require('elliptic').ec('p256'); | ||||||
|  | 
 | ||||||
|  |   function randomBytes(size) { | ||||||
|  |     return new OAUTH3.PromiseA(function (resolve, reject) { | ||||||
|  |       crypto.randomBytes(size, function (err, buf) { | ||||||
|  |         if (err) { | ||||||
|  |           reject(err); | ||||||
|  |         } else { | ||||||
|  |           resolve(buf); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function sha256(buf) { | ||||||
|  |     return crypto.createHash('sha256').update(buf).digest(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function pbkdf2(password, salt) { | ||||||
|  |     // Derived AES key is 128 bit, and the function takes a size in bytes.
 | ||||||
|  |     return crypto.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256'); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function encrypt(key, iv, data) { | ||||||
|  |     var cipher = crypto.createCipheriv('aes-128-gcm', Buffer(key), Buffer(iv)); | ||||||
|  | 
 | ||||||
|  |     return Buffer.concat([cipher.update(Buffer(data)), cipher.final(), cipher.getAuthTag()]); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function decrypt(key, iv, data) { | ||||||
|  |     var decipher = crypto.createDecipheriv('aes-128-gcm', Buffer(key), Buffer(iv)); | ||||||
|  | 
 | ||||||
|  |     decipher.setAuthTag(Buffer(data.slice(-16))); | ||||||
|  |     return Buffer.concat([decipher.update(Buffer(data.slice(0, -16))), decipher.final()]); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function bnToBuffer(bn, size) { | ||||||
|  |     var buf = bn.toArrayLike(Buffer); | ||||||
|  | 
 | ||||||
|  |     if (!size || buf.length === size) { | ||||||
|  |       return buf; | ||||||
|  |     } else if (buf.length < size) { | ||||||
|  |       return Buffer.concat([Buffer(size-buf.length).fill(0), buf]); | ||||||
|  |     } else if (buf.length > size) { | ||||||
|  |       throw new Error('EC signature number bigger than expected'); | ||||||
|  |     } | ||||||
|  |     throw new Error('invalid size "'+size+'" converting BigNumber to Buffer'); | ||||||
|  |   } | ||||||
|  |   function bnToB64(bn) { | ||||||
|  |     var b64 = bnToBuffer(bn).toString('base64'); | ||||||
|  |     return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/, ''); | ||||||
|  |   } | ||||||
|  |   function genEcdsaKeyPair() { | ||||||
|  |     var key = ec.genKeyPair(); | ||||||
|  |     var pubJwk = { | ||||||
|  |       key_ops: ['verify'] | ||||||
|  |     , kty: 'EC' | ||||||
|  |     , crv: 'P-256' | ||||||
|  |     , x: bnToB64(key.getPublic().getX()) | ||||||
|  |     , y: bnToB64(key.getPublic().getY()) | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     var privJwk = JSON.parse(JSON.stringify(pubJwk)); | ||||||
|  |     privJwk.key_ops = ['sign']; | ||||||
|  |     privJwk.d = bnToB64(key.getPrivate()); | ||||||
|  | 
 | ||||||
|  |     return {privateKey: privJwk, publicKey: pubJwk}; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function sign(jwk, msg) { | ||||||
|  |     var key = ec.keyFromPrivate(Buffer(jwk.d, 'base64')); | ||||||
|  |     var sig = key.sign(sha256(msg)); | ||||||
|  |     return Buffer.concat([bnToBuffer(sig.r, 32), bnToBuffer(sig.s, 32)]); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function verify(jwk, msg, signature) { | ||||||
|  |     var key = ec.keyFromPublic({x: Buffer(jwk.x, 'base64'), y: Buffer(jwk.y, 'base64')}); | ||||||
|  |     var sig = { | ||||||
|  |       r: Buffer(signature.slice(0, signature.length/2)) | ||||||
|  |     , s: Buffer(signature.slice(signature.length/2)) | ||||||
|  |     }; | ||||||
|  |     return key.verify(sha256(msg), sig); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function promiseWrap(func) { | ||||||
|  |     return function() { | ||||||
|  |       var args = arguments; | ||||||
|  |       return new OAUTH3.PromiseA(function (resolve) { | ||||||
|  |         resolve(func.apply(null, args)); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |   exports.sha256  = promiseWrap(sha256); | ||||||
|  |   exports.pbkdf2  = promiseWrap(pbkdf2); | ||||||
|  |   exports.encrypt = promiseWrap(encrypt); | ||||||
|  |   exports.decrypt = promiseWrap(decrypt); | ||||||
|  |   exports.sign    = promiseWrap(sign); | ||||||
|  |   exports.verify  = promiseWrap(verify); | ||||||
|  |   exports.genEcdsaKeyPair = promiseWrap(genEcdsaKeyPair); | ||||||
|  |   exports.randomBytes = randomBytes; | ||||||
|  | }()); | ||||||
							
								
								
									
										17
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								package.json
									
									
									
									
									
								
							| @ -4,6 +4,7 @@ | |||||||
|   "description": "The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation.", |   "description": "The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation.", | ||||||
|   "main": "oauth3.node.js", |   "main": "oauth3.node.js", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|  |     "install": "./node_modules/.bin/gulp", | ||||||
|     "test": "echo \"Error: no test specified\" && exit 1" |     "test": "echo \"Error: no test specified\" && exit 1" | ||||||
|   }, |   }, | ||||||
|   "repository": { |   "repository": { | ||||||
| @ -30,6 +31,22 @@ | |||||||
|     "log", |     "log", | ||||||
|     "sign" |     "sign" | ||||||
|   ], |   ], | ||||||
|  |   "dependencies": { | ||||||
|  |     "elliptic": "^6.4.0" | ||||||
|  |   }, | ||||||
|  |   "devDependencies": { | ||||||
|  |     "browserify-aes": "^1.0.6", | ||||||
|  |     "create-hash": "^1.1.2", | ||||||
|  |     "pbkdf2": "^3.0.9", | ||||||
|  | 
 | ||||||
|  |     "browserify": "^14.1.0", | ||||||
|  |     "gulp": "^3.9.1", | ||||||
|  |     "gulp-cli": "^1.2.2", | ||||||
|  |     "gulp-rename": "^1.2.2", | ||||||
|  |     "gulp-streamify": "^1.0.2", | ||||||
|  |     "gulp-uglify": "^2.1.0", | ||||||
|  |     "vinyl-source-stream": "^1.1.0" | ||||||
|  |   }, | ||||||
|   "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", |   "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", | ||||||
|   "license": "(MIT OR Apache-2.0)" |   "license": "(MIT OR Apache-2.0)" | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user