Fixed test cases for IndexedDB use & some bug fixes

This commit is contained in:
mhelander 2017-09-16 15:47:56 +03:00
parent 5b2dc770f6
commit 22f07734fc
4 changed files with 276 additions and 117 deletions

View File

@ -75,11 +75,18 @@
<script>
var gun = Gun(location.origin+'/gun');
var user = gun.user && gun.user();
if (user) {
if(user){
// 1st: call create. 2nd call auth. After that, call recall
// user.create('dude', 'my secret').then(function(ack) { console.log('created ack:', ack) })
// user.auth('dude', 'my secret').then(function(user) { console.log('authenticated user:', user) })
// user.recall().then(function(ack) { console.log('recall ack:', ack) });
// user.create('dude', 'my secret').then(function(ack) { console.log('created ack:', ack) });
// user.recall({session: false}).then(function(ack){
// if (!ack || !ack.sea){
// console.log('user.recall not bootstrapping...');
// user.auth('dude', 'my secret', {pin: 'PIN'}).then(function(user) { console.log('authenticated user:', user) });
// // user.auth('dude', 'my secret', {pin: 'PIN', newpass: 'my secret'}).then(function(user) { console.log('authenticated user:', user) });
// } else {
// console.log('user.recall authenticated user:', ack.alias);
// }
// });
}
var chat = gun.get('converse');
chat.map().val(function(msg, field){

View File

@ -57,6 +57,7 @@
"buffer": "^5.0.7",
"eccrypto": "^1.0.3",
"express": ">=4.15.2",
"fake-indexeddb": "^2.0.3",
"hapi": "^16.1.1",
"inert": "^4.2.0",
"ip": "^1.1.5",

77
sea.js
View File

@ -22,7 +22,7 @@
}
var subtle, TextEncoder, TextDecoder, getRandomBytes;
var localStorage, sessionStorage, indexedDB;
var sessionStorage, indexedDB;
if(typeof window !== 'undefined'){
var wc = window.crypto || window.msCrypto; // STD or M$
@ -30,7 +30,6 @@
getRandomBytes = function(len){ return wc.getRandomValues(new Buffer(len)) };
TextEncoder = window.TextEncoder;
TextDecoder = window.TextDecoder;
localStorage = window.localStorage;
sessionStorage = window.sessionStorage;
indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB
|| window.msIndexedDB || window.shimIndexedDB;
@ -40,9 +39,8 @@
TextEncoder = require('text-encoding').TextEncoder;
TextDecoder = require('text-encoding').TextDecoder;
// Let's have Storage for NodeJS / testing
localStorage = new require('node-localstorage').LocalStorage('local');
sessionStorage = new require('node-localstorage').LocalStorage('session');
indexedDB = undefined; // TODO: simulate IndexedDB in NodeJS but how?
indexedDB = require("fake-indexeddb");
}
// Encryption parameters - TODO: maybe to be changed via init?
@ -179,7 +177,7 @@
}
function callOnStore(fn_, resolve_){
var open = indexedDB.open("GunDB", 1); // Open (or create) the database; 1 === 'version'
var open = indexedDB.open('GunDB', 1); // Open (or create) the database; 1 === 'version'
open.onupgradeneeded = function(){ // Create the schema; props === current version
var db = open.result;
db.createObjectStore('SEA', {keyPath: 'id'});
@ -211,21 +209,37 @@
sessionStorage.setItem('user', props.alias);
sessionStorage.setItem('remember', signed);
if(!encrypted){
localStorage.removeItem('remember');
return new Promise(function(resolve){
callOnStore(function(store) {
var act = store.clear(); // Wipes whole IndexedDB
act.onsuccess = function(){};
}, function(){ resolve() });
});
}
}).then(function(){
return !encrypted || SEA.enc(encrypted, pin).then(function(encrypted){
return encrypted && SEA.write(encrypted, priv).then(function(encsig){
localStorage.setItem('remember', encsig);
return new Promise(function(resolve){
callOnStore(function(store){
store.put({id: props.alias, auth: encsig});
}, function(){ resolve() });
});
}).catch(reject);
}).catch(reject);
}).then(function(){ resolve(props) })
.catch(function(e){ reject({err: 'Session persisting failed!'}) });
} else {
localStorage.removeItem('remember');
sessionStorage.removeItem('user');
sessionStorage.removeItem('remember');
return new Promise(function(resolve){
callOnStore(function(store) {
var act = store.clear(); // Wipes whole IndexedDB
act.onsuccess = function(){};
}, function(){ resolve() });
}).then(function(){
sessionStorage.removeItem('user');
sessionStorage.removeItem('remember');
resolve(props);
});
}
resolve(props);
});
};
}
@ -317,8 +331,19 @@
if(Gun.obj.has(props, 'pin') && Gun.obj.has(props, 'alias')
&& props.alias === alias){
pin = props.pin; // Got PIN so get localStorage secret if signature is ok
return readAndDecrypt(localStorage.getItem('remember'), pub, pin)
.then(checkRememberData); // And return proof if for matching alias
return new Promise(function(resolve){
var remember;
callOnStore(function(store) {
var getData = store.get(alias);
getData.onsuccess = function(){
remember = getData.result && getData.result.auth;
};
}, function(){ // And return proof if for matching alias
return readAndDecrypt(remember, pub, pin)
.then(checkRememberData).then(resolve)
.catch(function(){ resolve() });
});
});
}
// No PIN, let's try short-term proof if for matching alias
return checkRememberData(props);
@ -348,7 +373,8 @@
});
});
}).then(function(user){
finalizelogin(alias, user, root).then(resolve).catch(function(e){
pin = pin && {pin: pin};
finalizelogin(alias, user, root, pin).then(resolve).catch(function(e){
Gun.log('Failed to finalize login with new password!');
reject({
err: 'Finalizing new password login failed! Reason: '+(e && e.err) || e || ''
@ -358,9 +384,20 @@
reject({err: 'No authentication session found!'});
});
}
reject({
err: (localStorage.getItem('remember') && 'Missing PIN and alias!')
|| 'No authentication session found!'});
if(!alias){
return reject({err: 'No authentication session found!'});
}
var gotRemember;
callOnStore(function(store) {
var getData = store.get(alias);
getData.onsuccess = function(){
gotRemember = getData.result && getData.result.auth;
};
}, function(){ // And return proof if for matching alias
reject({
err: (gotRemember && 'Missing PIN and alias!')
|| 'No authentication session found!'});
});
});
}
@ -393,10 +430,11 @@
o
); };
return new Promise(function(resolve){
if(authsettings.validity && Gun.obj.has(p, 'pub') && Gun.obj.has(p, 'key')){
if(authsettings.validity && typeof window !== 'undefined'
&& Gun.obj.has(p, 'pub') && Gun.obj.has(p, 'key')){
var importAndStoreKey = function(){ // Creates new CryptoKey & stores it
importKey(p).then(function(key){ callOnStore(function(store){
store.put({id: p.pub, key: key, auth: 'just testing'});
store.put({id: p.pub, key: key});
}, function(){ resolve(key) }); });
};
if(Gun.obj.has(p, 'set')){ return importAndStoreKey() } // proof update so overwrite
@ -603,6 +641,7 @@
validity = v * 60; // minutes to seconds
}
}
var doIt = function(resolve, reject){
// opts = { hook: function({ iat, exp, alias, proof }),
// session: false } // true disables PIN requirement/support

View File

@ -7,8 +7,43 @@ var root;
(function(env){
root = env.window ? env.window : global;
root.indexedDB = require("fake-indexeddb");
}(this));
if(typeof Buffer === 'undefined'){
var Buffer = require('buffer').Buffer;
}
function callOnStore(fn_, resolve_){
var open = indexedDB.open('GunDB', 1); // Open (or create) the database; 1 === 'version'
open.onupgradeneeded = function(){ // Create the schema; props === current version
var db = open.result;
db.createObjectStore('SEA', {keyPath: 'id'});
};
open.onsuccess = function(){ // Start a new transaction
var db = open.result;
var tx = db.transaction('SEA', 'readwrite');
var store = tx.objectStore('SEA');
fn_(store);
tx.oncomplete = function(){ // Close the db when the transaction is done
db.close();
if(typeof resolve_ === 'function'){ resolve_() }
};
};
}
function checkIndexedDB(key, prop, resolve_){
var result;
callOnStore(function(store) {
var getData = store.get(key);
getData.onsuccess = function(){
result = getData.result && getData.result[prop];
};
}, function(){
resolve_(result);
});
}
Gun.SEA && describe('SEA', function(){
console.log('TODO: SEA! THIS IS AN EARLY ALPHA!!!');
var alias = 'dude';
@ -223,9 +258,13 @@ Gun().user && describe('Gun', function(){
if(wipeStorageData){
// ... and persisted session
localStorage.removeItem('remember');
// localStorage.removeItem('remember');
sessionStorage.removeItem('remember');
sessionStorage.removeItem('alias');
callOnStore(function(store) {
var act = store.clear(); // Wipes whole IndexedDB
act.onsuccess = function(){};
});
}
};
@ -283,15 +322,20 @@ Gun().user && describe('Gun', function(){
describe('auth', function(){
var checkStorage = function(done, hasPin){
return function(){
expect(root.sessionStorage.getItem('user')).to.not.be(undefined);
expect(root.sessionStorage.getItem('user')).to.not.be('');
var alias = root.sessionStorage.getItem('user');
expect(alias).to.not.be(undefined);
expect(alias).to.not.be('');
expect(root.sessionStorage.getItem('remember')).to.not.be(undefined);
expect(root.sessionStorage.getItem('remember')).to.not.be('');
if(hasPin){
expect(root.localStorage.getItem('remember')).to.not.be(undefined);
expect(root.localStorage.getItem('remember')).to.not.be('');
if(!hasPin){
return done();
}
done();
checkIndexedDB(alias, 'auth', function(auth){
expect(auth).to.not.be(undefined);
expect(auth).to.not.be('');
done();
});
};
};
@ -402,10 +446,10 @@ Gun().user && describe('Gun', function(){
it('with PIN auth session stored to sessionStorage', function(done){
if(type === 'callback'){
user.auth(alias+type, pass+' new', checkStorage(done, true), {pin: 'PIN'});
user.auth(alias+type, pass+' new', checkStorage(done/*, true*/), {pin: 'PIN'});
} else {
user.auth(alias+type, pass+' new', {pin: 'PIN'})
.then(checkStorage(done, true)).catch(done);
.then(checkStorage(done/*, true*/)).catch(done);
}
});
});
@ -548,35 +592,45 @@ Gun().user && describe('Gun', function(){
expect(user).to.not.be('');
expect(sRemember).to.not.be(undefined);
expect(sRemember).to.not.be('');
if(hasPin){
var lRemember = root.localStorage.getItem('remember');
expect(lRemember).to.not.be(undefined);
expect(lRemember).to.not.be('');
}
// NOTE: done can be Promise returning function
var ret;
if (wantAck) {
[ 'err', 'pub', 'sea', 'alias', 'put' ].forEach(function(key){
if(wantAck && ack){
['err', 'pub', 'sea', 'alias', 'put'].forEach(function(key){
if(typeof ack[key] !== 'undefined'){
(ret = ret || {})[key] = ack[key];
}
});
}
return done(ret);
// NOTE: done can be Promise returning function
return !hasPin || !wantAck || !ack ? done(ret)
: new Promise(function(resolve){
checkIndexedDB(ack.alias, 'auth', function(auth){
expect(auth).to.not.be(undefined);
expect(auth).to.not.be('');
resolve(done(wantAck && Object.assign(ret || {}, {auth: auth})));
});
});
};
};
// This re-constructs 'remember-me' data modified by manipulate func
var manipulateStorage = function(manipulate, hasPin){
var manipulateStorage = function(manipulate, pin){
expect(typeof manipulate).to.be('function');
// We'll use Gun internal User data
var usr = gun.back(-1)._.user;
expect(usr).to.not.be(undefined);
expect(usr).to.have.key('_');
expect(usr._).to.have.keys([ 'pub', 'sea' ]);
expect(usr._).to.have.keys(['pub', 'sea']);
// ... to validate 'remember' data
var remember = hasPin ? localStorage.getItem('remember')
: sessionStorage.getItem('remember');
return Gun.SEA.read(remember, usr._.pub).then(function(props){
pin = pin && new Buffer(pin, 'utf8').toString('base64');
return !pin ? Promise.resolve(sessionStorage.getItem('remember'))
: new Promise(function(resolve){
checkIndexedDB(usr._.alias, 'auth', resolve);
}).then(function(remember){
return Gun.SEA.read(remember, usr._.pub).then(function(props){
return !pin ? props
: Gun.SEA.dec(props, pin);
});
}).then(function(props){
try{ props && (props = JSON.parse(props)) }catch(e){} //eslint-disable-line no-empty
return props;
}).then(manipulate).then(function(props){
@ -584,8 +638,14 @@ Gun().user && describe('Gun', function(){
expect(props).to.not.be('');
return Gun.SEA.write(JSON.stringify(props), usr._.sea)
.then(function(remember){
return hasPin ? sessionStorage.setItem('remember', remember)
: sessionStorage.setItem('remember', remember);
return !pin ? sessionStorage.setItem('remember', remember)
: Gun.SEA.enc(remember, pin).then(function(encauth){
return new Promise(function(resolve){
callOnStore(function(store){
store.put({id: usr._.alias, auth: encauth});
}, resolve);
});
});
});
});
};
@ -672,10 +732,10 @@ Gun().user && describe('Gun', function(){
});
it('valid localStorage session bootstrap', function(done){
var sUser;
var sRemember;
var iAuth;
user.auth(alias+type, pass+' new', {pin: 'PIN'}).then(function(usr){
var sUser;
var sRemember;
var lRemember;
try{
expect(usr).to.not.be(undefined);
expect(usr).to.not.be('');
@ -689,49 +749,76 @@ Gun().user && describe('Gun', function(){
sUser = root.sessionStorage.getItem('user');
sRemember = root.sessionStorage.getItem('remember');
lRemember = root.localStorage.getItem('remember');
}catch(e){ done(e); return }
user.leave().then(function(ack){
try{
expect(ack).to.have.key('ok');
expect(gun.back(-1)._.user).to.not.have.keys([ 'sea', 'pub' ]);
expect(root.sessionStorage.getItem('user')).to.not.be(sUser);
expect(root.sessionStorage.getItem('remember')).to.not.be(sRemember);
expect(root.localStorage.getItem('remember')).to.not.be(lRemember);
}catch(e){ done(e); return }
root.sessionStorage.setItem('user', sUser);
root.sessionStorage.setItem('remember', sRemember);
root.localStorage.setItem('remember', lRemember);
user.recall(12 * 60, {session: false}).then(doCheck(done))
.catch(done);
}).catch(done);
}).catch(done);
});
it('valid localStorage session bootstraps using PIN', function(done){
user.recall(12 * 60, {session: false}).then(function(){
return user.auth(alias+type, pass+' new', {pin: 'PIN'});
}).then(doCheck(function(){
// Let's save remember props
var sUser = root.sessionStorage.getItem('user');
var sRemember = root.sessionStorage.getItem('remember');
var lRemember = root.localStorage.getItem('remember');
// Then logout user
return new Promise(function(resolve){
checkIndexedDB(sUser, 'auth', function(auth){ resolve(iAuth = auth) });
});
}).then(function(){
return user.leave().then(function(ack){
try{
expect(ack).to.have.key('ok');
expect(gun.back(-1)._.user).to.not.have.keys([ 'sea', 'pub' ]);
expect(root.sessionStorage.getItem('user')).to.not.be(sUser);
expect(root.sessionStorage.getItem('remember')).to.not.be(sRemember);
expect(root.localStorage.getItem('remember')).to.not.be(lRemember);
}catch(e){ done(e); return }
// Then restore localStorage remember data, skip sessionStorage
root.localStorage.setItem('remember', lRemember);
return new Promise(function(resolve){
checkIndexedDB(sUser, 'auth', function(auth){
expect(auth).to.not.be(iAuth);
resolve();
});
});
}).then(function(){
root.sessionStorage.setItem('user', sUser);
root.sessionStorage.setItem('remember', sRemember);
return new Promise(function(resolve){
callOnStore(function(store){
store.put({id: sUser, auth: iAuth});
}, resolve);
});
}).then(function(){
user.recall(12 * 60, {session: false}).then(doCheck(done))
.catch(done);
}).catch(done);
}).catch(done);
});
it('valid IndexedDB session bootstraps using PIN', function(done){
user.recall(12 * 60, {session: false}).then(function(){
return user.auth(alias+type, pass+' new', {pin: 'PIN'});
}).then(doCheck(function(ack){
// Let's save remember props
var sUser = root.sessionStorage.getItem('user');
var sRemember = root.sessionStorage.getItem('remember');
var iAuth = ack.auth;
return new Promise(function(resolve){
checkIndexedDB(sUser, 'auth', function(auth){
iAuth = auth;
resolve(user.leave()); // Then logout user
});
}).then(function(ack){
try{
expect(ack).to.have.key('ok');
expect(gun.back(-1)._.user).to.not.have.keys([ 'sea', 'pub' ]);
expect(root.sessionStorage.getItem('user')).to.not.be(sUser);
expect(root.sessionStorage.getItem('remember')).to.not.be(sRemember);
}catch(e){ done(e); return }
return new Promise(function(resolve){
checkIndexedDB(sUser, 'auth', function(auth){
try{ expect(auth).to.not.be(iAuth) }catch(e){ done(e) }
// Then restore IndexedDB auth data, skip sessionStorage
callOnStore(function(store){
store.put({id: sUser, auth: iAuth});
}, function(){
root.sessionStorage.setItem('user', sUser);
resolve(ack);
});
});
});
});
}, true)).then(function(){
}, true, true)).then(function(){
// Then try to recall authentication
return user.recall(12 * 60, {session: false}).then(function(props){
try{
@ -757,25 +844,40 @@ Gun().user && describe('Gun', function(){
}, true, true)).catch(done);
});
it('valid localStorage session fails to bootstrap using wrong PIN',
it('valid IndexedDB session fails to bootstrap using wrong PIN',
function(done){
user.recall(12 * 60, {session: false}).then(function(){
return user.auth(alias+type, pass+' new', {pin: 'PIN'});
}).then(doCheck(function(){
}).then(doCheck(function(ack){
var sUser = root.sessionStorage.getItem('user');
var sRemember = root.sessionStorage.getItem('remember');
var lRemember = root.localStorage.getItem('remember');
return user.leave().then(function(ack){
var iAuth = ack.auth;
return new Promise(function(resolve){
checkIndexedDB(sUser, 'auth', function(auth){
iAuth = auth;
resolve(user.leave()); // Then logout user
});
}).then(function(ack){
try{
expect(ack).to.have.key('ok');
expect(gun.back(-1)._.user).to.not.have.keys([ 'sea', 'pub' ]);
expect(root.sessionStorage.getItem('user')).to.not.be(sUser);
expect(root.sessionStorage.getItem('remember')).to.not.be(sRemember);
expect(root.localStorage.getItem('remember')).to.not.be(lRemember);
}catch(e){ done(e); return }
root.localStorage.setItem('remember', lRemember);
return new Promise(function(resolve){
checkIndexedDB(sUser, 'auth', function(auth){
try{ expect(auth).to.not.be(iAuth) }catch(e){ done(e) }
// Then restore IndexedDB auth data, skip sessionStorage
callOnStore(function(store){
store.put({id: sUser, auth: iAuth});
}, function(){
root.sessionStorage.setItem('user', sUser);
resolve(ack);
});
});
});
});
}, true)).then(function(){
}, true, true)).then(function(){
// Ok, time to try auth with alias & PIN
return user.auth(alias+type, undefined, {pin: 'PiN'});
}).then(function(){
@ -794,18 +896,19 @@ Gun().user && describe('Gun', function(){
});
it('expired session fails to bootstrap', function(done){
user.recall(60, {session: true}).then(function(){
return user.auth(alias+type, pass+' new');
var pin = 'PIN';
user.recall(60, {session: false}).then(function(){
return user.auth(alias+type, pass+' new', {pin: pin});
}).then(doCheck(function(){
// Storage data OK, let's back up time of auth to exp + 65 seconds
return manipulateStorage(function(props){
var ret = Object.assign({}, props, {iat: props.iat - 65 - props.exp});
return ret;
}, false);
}, pin);
})).then(function(){
// Simulate browser reload
throwOutUser();
user.recall(60, {session: true}).then(function(ack){
user.recall(60, {session: false}).then(function(ack){
expect(ack).to.not.be(undefined);
expect(ack).to.not.be('');
expect(ack).to.not.have.keys([ 'pub', 'sea' ]);
@ -820,34 +923,37 @@ Gun().user && describe('Gun', function(){
});
it('changed password', function(done){
var pin = 'PIN';
var sUser;
var sRemember;
var iAuth;
user.recall(60, {session: false}).then(function(){
return user.auth(alias+type, pass+' new', {pin: 'PIN'});
return user.auth(alias+type, pass+' new', {pin: pin});
}).then(function(usr){
var sUser;
var sRemember;
var lRemember;
try{
expect(usr).to.not.be(undefined);
expect(usr).to.not.be('');
expect(usr).to.not.have.key('err');
expect(usr).to.have.key('put');
expect(root.sessionStorage.getItem('user')).to.be(alias+type);
expect(root.sessionStorage.getItem('remember')).to.not.be(undefined);
expect(root.sessionStorage.getItem('remember')).to.not.be('');
expect(root.localStorage.getItem('remember')).to.not.be(undefined);
expect(root.localStorage.getItem('remember')).to.not.be('');
sUser = root.sessionStorage.getItem('user');
sRemember = root.sessionStorage.getItem('remember');
lRemember = root.localStorage.getItem('remember');
}catch(e){ done(e); return }
// Time to do new login with new password set
user.leave().then(function(ack){
try{
expect(ack).to.have.key('ok');
}catch(e){ done(e); return }
expect(sUser).to.be(alias+type);
return user.auth(alias+type, pass+' new', {newpass: pass, pin: 'PIN'})
sRemember = root.sessionStorage.getItem('remember');
expect(sRemember).to.not.be(undefined);
expect(sRemember).to.not.be('');
expect(root.localStorage.getItem('remember')).to.not.be(undefined);
expect(root.localStorage.getItem('remember')).to.not.be('');
}catch(e){ done(e); return }
return new Promise(function(resolve){
checkIndexedDB(sUser, 'auth', function(auth){ resolve(iAuth = auth) });
});
}).then(function(){
return user.leave().then(function(ack){
try{ expect(ack).to.have.key('ok') }catch(e){ done(e); return }
return user.auth(alias+type, pass+' new', {newpass: pass, pin: pin})
.then(function(usr){ expect(usr).to.not.have.key('err') });
}).then(function(){
return user.leave().then(function(ack){
@ -861,8 +967,13 @@ Gun().user && describe('Gun', function(){
// Call back previous remember data
root.sessionStorage.setItem('user', sUser);
root.sessionStorage.setItem('remember', sRemember);
root.localStorage.setItem('remember', lRemember);
return new Promise(function(resolve){
callOnStore(function(store){
store.put({id: sUser, auth: iAuth});
}, resolve);
});
}).then(function(){
user.recall(60, {session: false}).then(function(props){
expect(props).to.not.be(undefined);
expect(props).to.not.be('');
@ -878,6 +989,7 @@ Gun().user && describe('Gun', function(){
});
it('recall hook session manipulation', function(done){
var pin = 'PIN';
var exp;
var hookFunc = function(props){
exp = props.exp * 2;
@ -886,8 +998,8 @@ Gun().user && describe('Gun', function(){
resolve(ret);
});
};
user.recall(60, {session: true, hook: hookFunc}).then(function(){
return user.auth(alias+type, pass);
user.recall(60, {session: false, hook: hookFunc}).then(function(){
return user.auth(alias+type, pass, {pin: pin});
}).then(function(){
// Storage data OK, let's back up time of auth 65 minutes
return manipulateStorage(function(props){
@ -895,7 +1007,7 @@ Gun().user && describe('Gun', function(){
expect(props).to.have.key('exp');
expect(props.exp).to.be(exp);
return props;
}, false);
}, pin);
}).then(done).catch(done);
});
});