multiple device authentication !

main
jerl 2024-04-29 00:55:54 -05:00
parent 51f4536424
commit 15acf67922
14 changed files with 258 additions and 177 deletions

View File

@ -115,7 +115,7 @@ function newThread() {
document.getElementById('newthread').textContent = 'create';
} else {
window.threadmembers = [window.name];
document.getElementById('threads').insertAdjacentElement('afterend', html.node`
document.getElementById('home').insertAdjacentElement('afterend', html.node`
<form id='createthread' class='column' onsubmit=${createThread}>
<h3>create thread</h3>
<label for='newthreadname' class='heading'>thread name</label>
@ -147,8 +147,7 @@ function newThread() {
<br />
<button id='submitthread' type='submit'>create</button>
</form>
`
);
`);
document.getElementById('newthread').textContent = 'cancel';
}
}
@ -159,19 +158,29 @@ function clickedTab(event) {
}
render(document.body, html`
<div id='threads' class='column'>
<div id='home' class='column'>
<div id='threads'>
<h3>vybe</h3>
<h4>threads</h4>
<div id='threadlist'>loading...</div>
<button id='newthread' onclick=${newThread}>create</button>
</div>
<div id='user' onclick=${() =>
document.getElementById('profile').classList.toggle('hidden')
}>${window.name}</div>
</div>
<div id='profile' class='column hidden'>
<strong>authentication requests</strong>
<div id='authrequests'></div>
</div>
<hr class='separator' color='#666'>
<div id='thread' class='column'>
<div id='title'>
thread: <strong id='threadname'>meow</strong>
</div>
<div id='tabs'>
<button id='messagetab' class='tab active' onclick=${clickedTab}>messages</button><button id='spacetab' class='tab' onclick=${clickedTab}>space</button>
<button id='messagetab' class='tab active' onclick=${clickedTab}>messages
</button><button id='spacetab' class='tab' onclick=${clickedTab}>space</button>
</div>
<div id='message' class='tabcontent'></div>
<div id='space' class='tabcontent hidden'></div>
@ -200,3 +209,16 @@ window.emit('list_threads', {}, msg => {
threadlist.appendChild(makeThread(thread));
chooseThread.call(threadlist.firstChild);
});
window.socket.on('authrequest', (msg, respond) => {
const div = html.node`
<div class='authrequest'>
<button onclick=${() => {
respond(true);
div.remove();
}}>approve</button>
session <strong>${msg.id}</strong>
</div>`;
document.getElementById('authrequests').append(div);
setTimeout(() => div.remove(), Date.now() - msg.time + 60000 * 5);
});

View File

@ -18,7 +18,11 @@ async function auth() {
});
window.socket.emit(
'authenticate',
{ name: window.name, message: sig },
{
name: window.name,
message: sig,
pubkey: window.keys.armored.publicKey
},
msg => {
let register = document.getElementById('register');
if (!msg.success) {
@ -26,13 +30,45 @@ async function auth() {
register.classList.remove('hidden');
return;
}
if (register) {
register.remove();
localStorage.setItem('keys', JSON.stringify(window.keys.armored));
localStorage.setItem('name', window.name);
register.classList.add('hidden');
import('/app.js');
}
);
}
async function submit(event) {
event.preventDefault();
const name = document.getElementById('name').value;
if (!name)
return;
const keys = await openpgp.generateKey({
userIDs: [{ name }]
});
const priv = await openpgp.readKey({ armoredKey: keys.privateKey });
const pub = await openpgp.readKey({ armoredKey: keys.publicKey });
window.keys = { priv, pub, armored: keys };
window.name = name;
if (this.id === 'registerform') {
window.emit('create_user',
{ name, pubkey: keys.publicKey },
(msg) => {
if (!msg.success) {
document.getElementById('result').innerText = msg.message;
return;
}
auth();
}
);
}
else {
await auth();
document.getElementById('result').innerHTML =
`now open your profile on your other device to approve this authentication.
this session's ID is <strong>${pub.getFingerprint().slice(0,8)}</strong>`;
}
}
render(document.body, html`
<div id='register' class='hidden'>
@ -43,41 +79,21 @@ render(document.body, html`
for security, rather than passwords. your keys are stored in your
browser storage only, so do this on a browser you can access again.
</p>
<form onsubmit=${async e => {
e.preventDefault();
const name = document.getElementById('name').value;
if (!name)
return;
const keys = await openpgp.generateKey({
userIDs: [{ name }],
});
const priv = await openpgp.readKey({ armoredKey: keys.privateKey });
const pub = await openpgp.readKey({ armoredKey: keys.publicKey });
window.emit(
'create_user',
{ name, pubkey: keys.publicKey },
(msg) => {
if (!msg.success) {
document.querySelector('#message').innerText = msg.message;
return;
}
window.keys = { priv, pub };
localStorage.setItem('keys', JSON.stringify(keys));
localStorage.setItem('name', name);
window.name = name;
auth();
}
);
}} id='registerform'>
<p>
if you already have an account, enter your username here,
and you can authenticate using your other device.
</p>
<form onsubmit=${submit} id='registerform'>
<label for='name'>username: </label>
<input id='name' type='text' />
<button id='submit' type='submit'>generate keys & register</button>
<button id='submit' type='submit'>register</button>
<button onclick=${submit}>authenticate</button>
</form>
<p id='message'></p>
<p id='result'></p>
</div>
`);
async function gensession() {
function gensession() {
window.session = rand();
window.emit = (type, data, callback) =>
@ -94,7 +110,7 @@ async function gensession() {
window.onload = async () => {
window.socket = io();
await gensession();
gensession();
let keys = localStorage.getItem('keys');
if (keys) {
@ -102,7 +118,8 @@ window.onload = async () => {
keys = JSON.parse(keys);
window.keys = {
priv: await openpgp.readKey({ armoredKey: keys.privateKey }),
pub: await openpgp.readKey({ armoredKey: keys.publicKey })
pub: await openpgp.readKey({ armoredKey: keys.publicKey }),
armored: keys
};
await auth();
}
@ -110,8 +127,8 @@ window.onload = async () => {
document.getElementById('register').classList.remove('hidden');
window.socket.io.on('reconnect', async attempt => {
await gensession();
if (localStorage.getItem('keys'))
gensession();
if (window.keys)
await auth();
});
};

View File

@ -54,20 +54,21 @@
background: #1b1b1b;
outline: none;
border: 1px solid #444;
}
input:focus {
&:focus {
padding-bottom: 3px;
border-bottom: 3px solid #777;
}
input::placeholder {
&::placeholder {
color: #aaa;
}
}
#register {
margin-inline: 14px;
max-width: 800px;
}
.thread:hover,
.tab:hover {
.tab:hover,
#user:hover {
background-color: #303030;
}
.tab.active,
@ -90,13 +91,32 @@
.column {
flex: 1;
overflow: hidden;
margin: 5px;
}
.separator {
margin: 8px 2px;
}
#threads {
#home {
max-width: 250px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
#threads {
margin: 5px;
}
#user {
margin: 3px;
padding: 6px;
background-color: #191919;
}
#profile {
max-width: 250px;
> * {
margin: 5px;
}
}
.authrequest {
margin-block: 3px;
}
.thread {
padding: 2px 4px;
@ -113,7 +133,6 @@
#thread {
display: flex;
flex-direction: column;
margin: 0;
}
#title {
margin: 4px;
@ -143,14 +162,12 @@
#msginput {
display: flex;
flex-direction: row;
margin: 2px;
> * {
margin: 3px;
}
}
#msg {
flex-grow: 1;
margin: 2px;
}
#sendmsg {
margin: 2px 3px;
}
.message {
margin-bottom: 5px;

View File

@ -1,14 +1,14 @@
create table user (
id integer primary key asc,
name text,
pubkey text,
created timestamp default current_timestamp
);
create table authentication (
create table key (
user integer,
salt text,
pubkey text,
created timestamp default current_timestamp,
active boolean,
foreign key(user) references user(id)
);
@ -34,7 +34,6 @@ create table permission (
create table member (
thread integer,
user integer,
key_delivery text,
created timestamp default current_timestamp,
foreign key(user) references user(id),
foreign key(thread) references thread(id)

View File

@ -20,21 +20,27 @@ const io = new Server(server, {
},
});
io.cache = {};
io.on('connection', (socket) => {
for (let event in events) {
socket.on(event, (msg, callback) =>
events[event](msg, callback, socket, io)
);
socket.on(event, (msg, callback) => {
if (!events[event]) {
callback('no such event ' + event);
return;
}
events[event](msg, callback, socket);
});
}
socket.on('disconnect', reason => {
let sockets = io.cache[socket.username];
if (sockets)
sockets.splice(sockets.indexOf(socket.id), 1);
let user = vybe.users[socket.username];
if (user)
user.sockets.splice(user.sockets.indexOf(socket), 1);
})
});
global.vybe = {
users: {}
};
server.listen(PORT, () => {
console.log('server running on port ' + PORT);
});

View File

@ -1,27 +1,21 @@
const db = require('./db');
const authwrap = (fn) => async (msg, respond, socket, io) => {
const authwrap = (fn) => async (msg, respond, socket) => {
if (!respond)
respond = () => {};
if (!msg || !msg.__session) {
if (!msg || !msg.__session || socket.auth !== msg.__session) {
return respond({
success: false,
message: 'not authenticated',
message: 'not authenticated'
});
}
const result = await db.query(
`select user.* from user join authentication
on authentication.user = user.id
where authentication.salt = ?`,
[msg.__session]
);
if (result.rows.length === 0) {
return respond({
success: false,
message: 'user not found',
});
return await fn({
...msg,
auth_user: {
id: vybe.users[socket.username].id,
name: socket.username
}
return await fn({ ...msg, auth_user: result.rows[0] }, respond, socket, io);
}, respond, socket);
};
module.exports = authwrap;

View File

@ -1,27 +1,32 @@
const db = require('../db');
const openpgp = require('openpgp');
const authenticate = async (msg, respond, socket, io) => {
const authenticate = async (msg, respond, socket) => {
if (!msg.name || !msg.message) {
return respond({
success: false,
message: 'invalid message'
});
}
const result = await db.query('select * from user where name = ?', [
msg.name
]);
if (result.rows.length === 0) {
let userid = await db.query(
`select user.id from user where name = ?`, [msg.name]);
if (userid.rows.length === 0) {
return respond({
success: false,
message: 'user not found'
});
}
let result = await db.query(
`select user.id from user
join key on key.user = user.id
where name = ? and pubkey = ? and active = true`,
[msg.name, msg.pubkey]
);
try {
const key = await openpgp.readKey({ armoredKey: result.rows[0].pubkey });
const key = await openpgp.readKey({ armoredKey: msg.pubkey });
const verification = await openpgp.verify({
message: await openpgp.readCleartextMessage({
cleartextMessage: msg.message,
cleartextMessage: msg.message
}),
verificationKeys: key,
expectSigned: true
@ -33,31 +38,52 @@ const authenticate = async (msg, respond, socket, io) => {
message: 'bad auth message'
});
}
const auths = await db.query(
'select * from authentication where user = ? and salt = ?',
[result.rows[0].id, data[1]]
);
if (auths.rows.length === 0) {
await db.query('insert into authentication (user, salt) values (?, ?)', [
result.rows[0].id,
data[1]
]);
let user = vybe.users[msg.name];
if (!user)
user = vybe.users[msg.name] = {
id: userid.rows[0].id,
sockets: [],
authrequests: {}
};
if (result.rows.length === 0) {
// request auth from logged in sessions
let id = key.getFingerprint().slice(0, 8);
let time = Date.now();
if (!await new Promise(resolve => {
user.authrequests[id] = { time, callback: resolve };
for (let s of user.sockets)
s.emit('authrequest', { id, time }, resolve);
setTimeout(() => {
delete user.authrequests[id];
resolve(false);
}, 60000 * 5);
}))
return;
delete user.authrequests[id];
if (Date.now() - time > 60000 * 5)
return;
await db.query(
'insert into key (user, pubkey, active) values (?, ?, true)',
[user.id, msg.pubkey]);
}
// this socket is now authenticated
socket.auth = data[1];
socket.username = msg.name;
if (io.cache[msg.name]) {
io.cache[msg.name].push(socket.id);
} else {
io.cache[msg.name] = [socket.id];
}
return respond({
success: true,
});
} else {
return respond({
success: false,
message: 'already authenticated with this message'
user.sockets.push(socket);
respond({
success: true
});
// send authenticated session any auth requests
if (result.rows.length)
setTimeout(() => {
for (let id in user.authrequests)
socket.emit('authrequest', {
id,
time: user.authrequests[id].time
}, user.authrequests[id].callback);
}, 1000); // todo: do this better
}
} catch (err) {
catch (err) {
console.error('error in authentication: ' + err);
return respond({
success: false,

View File

@ -1,18 +1,18 @@
const db = require('../db');
const authwrap = require('../authwrap');
const create_thread = async (msg, respond, socket, io) => {
const create_thread = async (msg, respond, socket) => {
// validate inputs
if (typeof msg.name !== 'string') {
return respond({
success: false,
message: 'thread name required',
message: 'thread name required'
});
}
if (msg.name.length > 200) {
return respond({
success: false,
message: 'thread name 200 chars max',
message: 'thread name 200 chars max'
});
}
// add to db
@ -63,38 +63,37 @@ const create_thread = async (msg, respond, socket, io) => {
if (id.rows.length > 0) {
const user_id = id.rows[0].id;
await db.query(
'insert into member (thread, user, key_delivery) values (?, ?, ?)',
[thread_id, user_id, user.key]
'insert into member (thread, user) values (?, ?)',
[thread_id, user_id]
);
}
}
if (!msg.permissions || !msg.permissions.view_limited) {
for (let username in io.cache) {
for (let socket of io.cache[username]) {
io.to(socket).emit('new_thread', {
for (let username in vybe.users) {
for (let socket of vybe.users[username].sockets) {
socket.emit('new_thread', {
name: msg.name,
id: insert.rows[0].id,
permissions: {
is_member: false,
view: true,
post: !msg.permissions || !msg.permissions.post_limited
},
}
});
}
}
}
else {
for (let member of msg.members) {
for (let socket of io.cache[member.name]) {
io.to(socket).emit('new_thread', {
for (let socket of vybe.users[member.name].sockets) {
socket.emit('new_thread', {
name: msg.name,
id: insert.rows[0].id,
permissions: {
is_member: true,
view: true,
post: true
},
key: member.key
}
});
}
}
@ -102,7 +101,7 @@ const create_thread = async (msg, respond, socket, io) => {
// respond
return respond({
success: true,
id: insert.rows[0].id,
id: insert.rows[0].id
});
};

View File

@ -6,45 +6,49 @@ const create_user = async (msg, respond) => {
if (!msg.name) {
return respond({
success: false,
message: 'username required',
message: 'username required'
});
}
if (!msg.pubkey) {
return respond({
success: false,
message: 'public key required'
});
}
// ensure username is not taken
const result = await db.query('select * from user where name = ?', [
msg.name,
msg.name
]);
if (result.rows.length > 0) {
console.log(`username already exists: ${result}`);
return respond({
success: false,
message: 'a user with this name already exists on this server',
message: 'a user with this name already exists on this server'
});
}
// validate public key
if (!msg.pubkey) {
return respond({
success: false,
message: 'public key required',
});
}
try {
await openpgp.readKey({ armoredKey: msg.pubkey });
} catch (err) {
console.err('error in create_user readkey: ' + err);
return respond({
success: false,
message: 'public key invalid',
message: 'public key invalid'
});
}
// add to db
const insert = await db.query(
'insert into user (name, pubkey) values (?, ?) returning id',
[msg.name, msg.pubkey]
'insert into user (name) values (?) returning id',
[msg.name]
);
await db.query(
'insert into key (user, pubkey, active) values (?, ?, true)',
[insert.rows[0].id, msg.pubkey]
)
// respond
return respond({
success: true,
id: insert.rows[0].id,
id: insert.rows[0].id
});
};

View File

@ -1,30 +1,29 @@
const db = require('../db');
const authwrap = require('../authwrap');
const get_keys = async (msg, respond, socket, io) => {
const get_keys = async (msg, respond) => {
// validate inputs
if (!msg.names) {
return respond({
success: false,
message: 'user names required',
message: 'user names required'
});
}
if (typeof msg.names !== 'object') {
return respond({
success: false,
message: "can't iterate user names",
message: "can't iterate user names"
});
}
const keys = await db.query(
`select name, pubkey from user where name in (${msg.names
.map((i) => '?')
.join(',')})`,
`select name, pubkey from user where name in
(${msg.names.map((i) => '?').join(',')})`,
msg.names
);
// respond
return respond({
success: true,
keys: keys.rows,
keys: keys.rows
});
};

View File

@ -6,13 +6,13 @@ const get_space = async (msg, respond) => {
if (!msg.thread) {
return respond({
success: false,
message: 'thread ID required',
message: 'thread ID required'
});
}
if (!(await check_permission(msg.auth_user.id, msg.thread)).view) {
return respond({
success: false,
message: "you can't view this thread",
message: "you can't view this thread"
});
}
const spans = await db.query(

View File

@ -4,7 +4,7 @@ const check_permission = require('../check_permission');
const list_threads = async (msg, respond) => {
const threads = await db.query(
`select name, id, member.key_delivery as key from thread
`select name, id from thread
join permission on thread.id = permission.thread
left join member on thread.id = member.thread
where permission.permission = 'view'
@ -20,12 +20,12 @@ const list_threads = async (msg, respond) => {
for (let thread of threads.rows) {
rows.push({
...thread,
permissions: await check_permission(msg.auth_user.id, thread.id),
permissions: await check_permission(msg.auth_user.id, thread.id)
});
}
return respond({
success: true,
threads: rows,
threads: rows
});
};

View File

@ -2,7 +2,7 @@ const db = require('../db');
const authwrap = require('../authwrap');
const check_permission = require('../check_permission');
const save_span = async (msg, respond, socket, io) => {
const save_span = async (msg, respond, socket) => {
if (!msg.thread) {
return respond({
success: false,
@ -43,12 +43,11 @@ const save_span = async (msg, respond, socket, io) => {
"select * from permission where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'",
[msg.thread]
);
for (let username in io.cache) {
for (let username in vybe.users) {
if (permissions.rows.length > 0 || members.includes(username)) {
const sockets = io.cache[username];
for (let s of sockets) {
if (s !== socket.id)
io.to(s).emit('span', {
for (let s of vybe.users[username].sockets) {
if (s !== socket)
s.emit('span', {
id,
thread: msg.thread,
content: msg.content,

View File

@ -2,17 +2,17 @@ const db = require('../db');
const authwrap = require('../authwrap');
const check_permission = require('../check_permission');
const send_message = async (msg, respond, socket, io) => {
const send_message = async (msg, respond) => {
if (!msg.thread) {
return respond({
success: false,
message: 'thread ID required',
message: 'thread ID required'
});
}
if (!(await check_permission(msg.auth_user.id, msg.thread)).post) {
return respond({
success: false,
message: "you can't post to this thread",
message: "you can't post to this thread"
});
}
// add message and send it to everyone
@ -32,22 +32,21 @@ const send_message = async (msg, respond, socket, io) => {
"select * from permission where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'",
[msg.thread]
);
for (let username in io.cache) {
for (let username in vybe.users) {
if (permissions.rows.length > 0 || members.includes(username)) {
const sockets = io.cache[username];
for (let s of sockets) {
io.to(s).emit('new_message', {
for (let s of vybe.users[username].sockets) {
s.emit('new_message', {
id: id.rows[0].id,
name: msg.auth_user.name,
message: msg.message,
thread: msg.thread,
thread: msg.thread
});
}
}
}
return respond({
success: true,
id: id.rows[0].id,
id: id.rows[0].id
});
};