2019-04-18 00:50:03 +00:00
|
|
|
import * as srp from 'srp-js';
|
2016-10-21 17:20:36 +00:00
|
|
|
import * as crypt from './crypt';
|
2019-04-18 00:50:03 +00:00
|
|
|
import * as fetch from './fetch';
|
|
|
|
|
|
|
|
const loginCallbacks = [];
|
|
|
|
|
|
|
|
function _callCallbacks() {
|
|
|
|
const loggedIn = isLoggedIn();
|
|
|
|
console.log('[session] Sync state changed loggedIn=' + loggedIn);
|
|
|
|
for (const cb of loginCallbacks) {
|
|
|
|
if (typeof cb === 'function') {
|
|
|
|
cb(loggedIn);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function onLoginLogout(callback) {
|
|
|
|
loginCallbacks.push(callback);
|
|
|
|
}
|
2016-10-21 17:20:36 +00:00
|
|
|
|
2016-11-10 02:40:53 +00:00
|
|
|
/** Create a new session for the user */
|
2018-06-25 17:42:50 +00:00
|
|
|
export async function login(rawEmail, rawPassphrase) {
|
2016-10-21 17:20:36 +00:00
|
|
|
// ~~~~~~~~~~~~~~~ //
|
|
|
|
// Sanitize Inputs //
|
|
|
|
// ~~~~~~~~~~~~~~~ //
|
|
|
|
|
|
|
|
const email = _sanitizeEmail(rawEmail);
|
|
|
|
const passphrase = _sanitizePassphrase(rawPassphrase);
|
|
|
|
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
|
|
|
// Fetch Salt and Submit A To Server //
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
|
|
|
|
2019-05-29 20:38:16 +00:00
|
|
|
const { saltKey, saltAuth } = await _getAuthSalts(email);
|
2016-10-21 17:20:36 +00:00
|
|
|
const authSecret = await crypt.deriveKey(passphrase, email, saltKey);
|
|
|
|
const secret1 = await crypt.srpGenKey();
|
|
|
|
const c = new srp.Client(
|
|
|
|
_getSrpParams(),
|
|
|
|
Buffer.from(saltAuth, 'hex'),
|
|
|
|
Buffer.from(email, 'utf8'),
|
|
|
|
Buffer.from(authSecret, 'hex'),
|
2018-12-12 17:36:11 +00:00
|
|
|
Buffer.from(secret1, 'hex'),
|
2016-10-21 17:20:36 +00:00
|
|
|
);
|
|
|
|
const srpA = c.computeA().toString('hex');
|
2019-04-18 00:50:03 +00:00
|
|
|
const { sessionStarterId, srpB } = await fetch.post(
|
|
|
|
'/auth/login-a',
|
|
|
|
{
|
|
|
|
srpA,
|
|
|
|
email,
|
|
|
|
},
|
|
|
|
null,
|
|
|
|
);
|
2016-10-21 17:20:36 +00:00
|
|
|
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~ //
|
|
|
|
// Compute and Submit M1 //
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~ //
|
|
|
|
|
2017-11-20 16:07:36 +00:00
|
|
|
c.setB(Buffer.from(srpB, 'hex'));
|
2016-10-21 17:20:36 +00:00
|
|
|
const srpM1 = c.computeM1().toString('hex');
|
2019-04-18 00:50:03 +00:00
|
|
|
const { srpM2 } = await fetch.post(
|
|
|
|
'/auth/login-m1',
|
|
|
|
{
|
|
|
|
srpM1,
|
|
|
|
sessionStarterId,
|
|
|
|
},
|
|
|
|
null,
|
|
|
|
);
|
2016-10-21 17:20:36 +00:00
|
|
|
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
|
|
|
// Verify Server Identity M2 //
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
|
|
|
|
2017-11-20 16:07:36 +00:00
|
|
|
c.checkM2(Buffer.from(srpM2, 'hex'));
|
2016-10-21 17:20:36 +00:00
|
|
|
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~ //
|
|
|
|
// Initialize the Session //
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~ //
|
|
|
|
|
|
|
|
// Compute K (used for session ID)
|
|
|
|
const sessionId = c.computeK().toString('hex');
|
|
|
|
|
|
|
|
// Get and store some extra info (salts and keys)
|
|
|
|
const {
|
|
|
|
publicKey,
|
|
|
|
encPrivateKey,
|
|
|
|
encSymmetricKey,
|
|
|
|
saltEnc,
|
|
|
|
accountId,
|
2016-11-07 20:24:38 +00:00
|
|
|
firstName,
|
2018-12-12 17:36:11 +00:00
|
|
|
lastName,
|
2019-04-18 00:50:03 +00:00
|
|
|
} = await _whoami(sessionId);
|
2016-10-21 17:20:36 +00:00
|
|
|
|
|
|
|
const derivedSymmetricKey = await crypt.deriveKey(passphrase, email, saltEnc);
|
2018-10-17 16:42:33 +00:00
|
|
|
const symmetricKeyStr = await crypt.decryptAES(derivedSymmetricKey, JSON.parse(encSymmetricKey));
|
2016-10-21 17:20:36 +00:00
|
|
|
|
|
|
|
// Store the information for later
|
|
|
|
setSessionData(
|
|
|
|
sessionId,
|
|
|
|
accountId,
|
|
|
|
firstName,
|
2016-11-07 20:24:38 +00:00
|
|
|
lastName,
|
2016-10-21 17:20:36 +00:00
|
|
|
email,
|
|
|
|
JSON.parse(symmetricKeyStr),
|
|
|
|
JSON.parse(publicKey),
|
2018-12-12 17:36:11 +00:00
|
|
|
JSON.parse(encPrivateKey),
|
2016-10-21 17:20:36 +00:00
|
|
|
);
|
2017-01-09 21:59:52 +00:00
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
_callCallbacks();
|
2017-01-11 03:18:15 +00:00
|
|
|
}
|
|
|
|
|
2019-05-29 20:38:16 +00:00
|
|
|
export async function changePasswordWithToken(rawNewPassphrase, confirmationCode) {
|
|
|
|
// Sanitize inputs
|
|
|
|
const newPassphrase = _sanitizePassphrase(rawNewPassphrase);
|
|
|
|
const newEmail = getEmail(); // Use the same one
|
|
|
|
|
|
|
|
// Fetch some things
|
|
|
|
const { saltEnc, encSymmetricKey } = await _whoami();
|
|
|
|
const { saltKey, saltAuth } = await _getAuthSalts(newEmail);
|
|
|
|
|
|
|
|
// Generate some secrets for the user base'd on password
|
|
|
|
const newSecret = await crypt.deriveKey(newPassphrase, newEmail, saltEnc);
|
|
|
|
const newAuthSecret = await crypt.deriveKey(newPassphrase, newEmail, saltKey);
|
|
|
|
|
|
|
|
const newVerifier = srp
|
|
|
|
.computeVerifier(
|
|
|
|
_getSrpParams(),
|
|
|
|
Buffer.from(saltAuth, 'hex'),
|
|
|
|
Buffer.from(newEmail, 'utf8'),
|
|
|
|
Buffer.from(newAuthSecret, 'hex'),
|
|
|
|
)
|
|
|
|
.toString('hex');
|
|
|
|
|
|
|
|
// Re-encrypt existing keys with new secret
|
|
|
|
const newEncSymmetricKeyJSON = crypt.encryptAES(newSecret, _getSymmetricKey());
|
|
|
|
const newEncSymmetricKey = JSON.stringify(newEncSymmetricKeyJSON);
|
|
|
|
|
|
|
|
return fetch.post(
|
|
|
|
`/auth/change-password`,
|
|
|
|
{
|
|
|
|
code: confirmationCode,
|
|
|
|
newEmail: newEmail,
|
|
|
|
encSymmetricKey: encSymmetricKey,
|
|
|
|
newVerifier,
|
|
|
|
newEncSymmetricKey,
|
|
|
|
},
|
|
|
|
getCurrentSessionId(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function sendPasswordChangeCode() {
|
|
|
|
return fetch.post('/auth/send-password-code', null, getCurrentSessionId());
|
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
export function getPublicKey() {
|
2019-04-18 00:50:03 +00:00
|
|
|
return _getSessionData().publicKey;
|
2016-10-21 17:20:36 +00:00
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
export function getPrivateKey() {
|
2019-04-18 00:50:03 +00:00
|
|
|
const { symmetricKey, encPrivateKey } = _getSessionData();
|
2016-10-21 17:20:36 +00:00
|
|
|
const privateKeyStr = crypt.decryptAES(symmetricKey, encPrivateKey);
|
|
|
|
return JSON.parse(privateKeyStr);
|
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
export function getCurrentSessionId() {
|
2017-11-22 21:10:34 +00:00
|
|
|
if (window) {
|
2017-11-21 17:49:33 +00:00
|
|
|
return window.localStorage.getItem('currentSessionId');
|
|
|
|
} else {
|
2019-04-18 00:50:03 +00:00
|
|
|
return '';
|
2017-11-21 17:49:33 +00:00
|
|
|
}
|
2016-10-21 17:20:36 +00:00
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
export function getAccountId() {
|
2019-04-18 00:50:03 +00:00
|
|
|
return _getSessionData().accountId;
|
2016-10-21 17:20:36 +00:00
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
export function getEmail() {
|
2019-04-18 00:50:03 +00:00
|
|
|
return _getSessionData().email;
|
2016-10-21 17:20:36 +00:00
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
export function getFirstName() {
|
2019-04-18 00:50:03 +00:00
|
|
|
return _getSessionData().firstName;
|
2016-10-21 17:20:36 +00:00
|
|
|
}
|
|
|
|
|
2019-05-29 20:38:16 +00:00
|
|
|
export function getLastName() {
|
|
|
|
return _getSessionData().firstName;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getFullName() {
|
|
|
|
return `${getFirstName()} ${getLastName()}`.trim();
|
|
|
|
}
|
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
/** Check if we (think) we have a session */
|
|
|
|
export function isLoggedIn() {
|
|
|
|
return !!getCurrentSessionId();
|
2016-11-07 20:24:38 +00:00
|
|
|
}
|
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
/** Log out and delete session data */
|
|
|
|
export async function logout() {
|
|
|
|
try {
|
|
|
|
await fetch.post('/auth/logout', null, getCurrentSessionId());
|
|
|
|
} catch (e) {
|
|
|
|
// Not a huge deal if this fails, but we don't want it to prevent the
|
|
|
|
// user from signing out.
|
|
|
|
console.warn('Failed to logout', e);
|
2016-10-21 17:20:36 +00:00
|
|
|
}
|
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
_unsetSessionData();
|
|
|
|
_callCallbacks();
|
2016-10-21 17:20:36 +00:00
|
|
|
}
|
|
|
|
|
2016-11-10 02:40:53 +00:00
|
|
|
/** Set data for the new session and store it encrypted with the sessionId */
|
2018-06-25 17:42:50 +00:00
|
|
|
export function setSessionData(
|
|
|
|
sessionId,
|
|
|
|
accountId,
|
|
|
|
firstName,
|
|
|
|
lastName,
|
|
|
|
email,
|
|
|
|
symmetricKey,
|
|
|
|
publicKey,
|
2018-12-12 17:36:11 +00:00
|
|
|
encPrivateKey,
|
2018-06-25 17:42:50 +00:00
|
|
|
) {
|
2016-10-21 17:20:36 +00:00
|
|
|
const dataStr = JSON.stringify({
|
|
|
|
id: sessionId,
|
|
|
|
accountId: accountId,
|
|
|
|
symmetricKey: symmetricKey,
|
|
|
|
publicKey: publicKey,
|
|
|
|
encPrivateKey: encPrivateKey,
|
|
|
|
email: email,
|
|
|
|
firstName: firstName,
|
2018-12-12 17:36:11 +00:00
|
|
|
lastName: lastName,
|
2016-10-21 17:20:36 +00:00
|
|
|
});
|
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
window.localStorage.setItem(_getSessionKey(sessionId), dataStr);
|
2016-10-21 17:20:36 +00:00
|
|
|
|
|
|
|
// NOTE: We're setting this last because the stuff above might fail
|
2017-03-03 20:09:08 +00:00
|
|
|
window.localStorage.setItem('currentSessionId', sessionId);
|
2016-10-21 17:20:36 +00:00
|
|
|
}
|
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
export async function listTeams() {
|
|
|
|
return fetch.get('/api/teams', getCurrentSessionId());
|
2016-10-21 17:20:36 +00:00
|
|
|
}
|
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
export async function endTrial() {
|
2019-05-02 15:25:30 +00:00
|
|
|
await fetch.put('/api/billing/end-trial', null, getCurrentSessionId());
|
2016-10-21 17:20:36 +00:00
|
|
|
}
|
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
// ~~~~~~~~~~~~~~~~ //
|
|
|
|
// Helper Functions //
|
|
|
|
// ~~~~~~~~~~~~~~~~ //
|
2016-11-17 18:45:54 +00:00
|
|
|
|
2019-05-29 20:38:16 +00:00
|
|
|
function _getSymmetricKey() {
|
|
|
|
const sessionData = _getSessionData();
|
|
|
|
return sessionData.symmetricKey;
|
|
|
|
}
|
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
function _whoami(sessionId = null) {
|
|
|
|
return fetch.get('/auth/whoami', sessionId || getCurrentSessionId());
|
2016-10-21 17:20:36 +00:00
|
|
|
}
|
|
|
|
|
2019-05-29 20:38:16 +00:00
|
|
|
function _getAuthSalts(email) {
|
|
|
|
return fetch.post('/auth/login-s', { email }, getCurrentSessionId());
|
|
|
|
}
|
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
function _getSessionData() {
|
|
|
|
const sessionId = getCurrentSessionId();
|
|
|
|
if (!sessionId || !window) {
|
|
|
|
return {};
|
|
|
|
}
|
2016-12-21 23:37:48 +00:00
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
const dataStr = window.localStorage.getItem(_getSessionKey(sessionId));
|
|
|
|
return JSON.parse(dataStr);
|
2016-11-07 20:24:38 +00:00
|
|
|
}
|
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
function _unsetSessionData() {
|
|
|
|
const sessionId = getCurrentSessionId();
|
|
|
|
window.localStorage.removeItem(_getSessionKey(sessionId));
|
|
|
|
window.localStorage.removeItem(`currentSessionId`);
|
2017-01-11 03:18:15 +00:00
|
|
|
}
|
2016-10-21 17:20:36 +00:00
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
function _getSessionKey(sessionId) {
|
2017-03-03 20:09:08 +00:00
|
|
|
return `session__${(sessionId || '').slice(0, 10)}`;
|
2017-01-09 21:59:52 +00:00
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
function _getSrpParams() {
|
2016-10-21 17:20:36 +00:00
|
|
|
return srp.params[2048];
|
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
function _sanitizeEmail(email) {
|
2016-10-21 17:20:36 +00:00
|
|
|
return email.trim().toLowerCase();
|
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
function _sanitizePassphrase(passphrase) {
|
2016-10-21 17:20:36 +00:00
|
|
|
return passphrase.trim().normalize('NFKD');
|
|
|
|
}
|