Introduced master key system
This commit is contained in:
@@ -1,11 +1,43 @@
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
import { rawSubscriptionKeyStore } from './store';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function postSubscribeBackend(authorId: string) {
|
||||
const rawSubscriptionKey = get(rawSubscriptionKeyStore);
|
||||
|
||||
if (!rawSubscriptionKey) return;
|
||||
|
||||
await sodium.ready;
|
||||
|
||||
const rawKey = sodium.from_base64(rawSubscriptionKeyStore);
|
||||
|
||||
const internalAuthorId = sodium.crypto_generichash(
|
||||
sodium.crypto_generichash_BYTES,
|
||||
authorId,
|
||||
rawKey
|
||||
);
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
const channelIdNonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
const channelIdCipher = sodium.crypto_secretbox_easy(
|
||||
textEncoder.encode(authorId),
|
||||
channelIdNonce,
|
||||
rawKey
|
||||
);
|
||||
|
||||
const channelNameNonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
const channelNameCipher = sodium.crypto_secretbox_easy(
|
||||
textEncoder.encode(authorId),
|
||||
channelNameNonce,
|
||||
rawKey
|
||||
);
|
||||
}
|
||||
|
||||
export async function createUserBackend(username: string, rawPassword: string): Promise<boolean> {
|
||||
await sodium.ready;
|
||||
|
||||
const passwordSalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
|
||||
const loginHash = sodium.crypto_pwhash(
|
||||
32,
|
||||
rawPassword,
|
||||
@@ -15,7 +47,24 @@ export async function createUserBackend(username: string, rawPassword: string):
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
const subscriptionPasswordSalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
const decryptionKeySalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
const rawDecryptionKey = sodium.crypto_pwhash(
|
||||
32,
|
||||
rawPassword,
|
||||
decryptionKeySalt,
|
||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
const rawDecryptionMasterKey = sodium.randombytes_buf(sodium.crypto_secretbox_KEYBYTES);
|
||||
const decryptionMasterKeyNonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
|
||||
const masterKeyCipher = sodium.crypto_secretbox_easy(
|
||||
rawDecryptionMasterKey,
|
||||
decryptionMasterKeyNonce,
|
||||
rawDecryptionKey
|
||||
);
|
||||
|
||||
const userCreateResp = await fetch('/api/user/create', {
|
||||
method: 'POST',
|
||||
@@ -25,22 +74,17 @@ export async function createUserBackend(username: string, rawPassword: string):
|
||||
hash: sodium.to_base64(loginHash),
|
||||
salt: sodium.to_base64(passwordSalt)
|
||||
},
|
||||
subscriptionPasswordSalt: sodium.to_base64(subscriptionPasswordSalt)
|
||||
decryptionKeySalt: sodium.to_base64(decryptionKeySalt),
|
||||
masterKey: {
|
||||
cipher: sodium.to_base64(masterKeyCipher),
|
||||
nonce: sodium.to_base64(decryptionMasterKeyNonce)
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (!userCreateResp.ok) return false;
|
||||
|
||||
const subscriptionRawKey = sodium.crypto_pwhash(
|
||||
32,
|
||||
rawPassword,
|
||||
passwordSalt,
|
||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
rawSubscriptionKeyStore.set(sodium.to_base64(subscriptionRawKey));
|
||||
rawSubscriptionKeyStore.set(sodium.to_base64(rawDecryptionMasterKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -72,16 +116,24 @@ export async function loginUserBackend(username: string, rawPassword: string): P
|
||||
|
||||
if (!loginResp.ok) return false;
|
||||
|
||||
const subscriptionRawKey = sodium.crypto_pwhash(
|
||||
32,
|
||||
const loginJson = await loginResp.json();
|
||||
|
||||
const rawDecryptionKey = sodium.crypto_pwhash(
|
||||
sodium.crypto_secretbox_KEYBYTES,
|
||||
rawPassword,
|
||||
sodium.from_base64(passwordSalts.subscriptionPasswordSalt),
|
||||
sodium.from_base64(passwordSalts.decryptionKeySalt),
|
||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
rawSubscriptionKeyStore.set(sodium.to_base64(subscriptionRawKey));
|
||||
const rawDecryptionMasterKey = sodium.crypto_secretbox_open_easy(
|
||||
sodium.from_base64(loginJson.masterKeyCipher),
|
||||
sodium.from_base64(loginJson.masterKeyNonce),
|
||||
rawDecryptionKey
|
||||
);
|
||||
|
||||
rawSubscriptionKeyStore.set(sodium.to_base64(rawDecryptionMasterKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ export interface UserTableModel extends Model {
|
||||
passwordHash: string;
|
||||
passwordSalt: string;
|
||||
created: Date;
|
||||
subscriptionPasswordSalt: string;
|
||||
decryptionKeySalt: string;
|
||||
masterKeyCipher: string;
|
||||
masterKeyNonce: string;
|
||||
}
|
||||
|
||||
export const UserTable = sequelize.define('User', {
|
||||
@@ -38,27 +40,41 @@ export const UserTable = sequelize.define('User', {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
subscriptionPasswordSalt: {
|
||||
decryptionKeySalt: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
masterKeyCipher: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
masterKeyNonce: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
|
||||
export interface ChannelSubscriptionModel {
|
||||
id: string; // Hashed authId with subscription key on client.
|
||||
channelIdCipher: string;
|
||||
channelIdSalt: string;
|
||||
channelIdNonce: string;
|
||||
channelNameCipher: string;
|
||||
channelNameSalt: string;
|
||||
channelNameNonce: string;
|
||||
lastRSSFetch: Date;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export const ChannelSubscriptionTable = sequelize.define('Subscriptions', {
|
||||
id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
primaryKey: true
|
||||
},
|
||||
channelIdCipher: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
channelIdSalt: {
|
||||
channelIdNonce: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
@@ -66,7 +82,7 @@ export const ChannelSubscriptionTable = sequelize.define('Subscriptions', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
channelNameSalt: {
|
||||
channelNameNonce: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
|
||||
@@ -1,30 +1,35 @@
|
||||
import { UserTable, type ChannelSubscriptionModel, type UserTableModel } from './database';
|
||||
import {
|
||||
ChannelSubscriptionTable,
|
||||
UserTable,
|
||||
type ChannelSubscriptionModel,
|
||||
type UserTableModel
|
||||
} from './database';
|
||||
import { Op } from 'sequelize';
|
||||
import crypto from 'crypto';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export class User {
|
||||
private user: UserTableModel;
|
||||
data: UserTableModel;
|
||||
|
||||
constructor(user: UserTableModel) {
|
||||
this.user = user;
|
||||
constructor(data: UserTableModel) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public get id() {
|
||||
return this.user.id;
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
public get publicPasswordSalts() {
|
||||
return {
|
||||
subscriptionPasswordSalt: this.user.subscriptionPasswordSalt,
|
||||
passwordSalt: this.user.passwordSalt
|
||||
decryptionKeySalt: this.data.decryptionKeySalt,
|
||||
passwordSalt: this.data.passwordSalt
|
||||
};
|
||||
}
|
||||
|
||||
private get userWhere() {
|
||||
return {
|
||||
where: {
|
||||
[Op.or]: [{ id: this.user.id }, { username: this.user.username }]
|
||||
[Op.or]: [{ id: this.data.id }, { username: this.data.username }]
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -33,10 +38,35 @@ export class User {
|
||||
await UserTable.destroy(this.userWhere);
|
||||
}
|
||||
|
||||
async addSubscription(subscription: Omit<ChannelSubscriptionModel, 'userId'>) {
|
||||
await ChannelSubscriptionTable.create({
|
||||
...subscription,
|
||||
userId: this.id
|
||||
});
|
||||
}
|
||||
|
||||
async removeSubscription(id: string) {
|
||||
await ChannelSubscriptionTable.destroy({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async amSubscribed(id: string): Promise<boolean> {
|
||||
return (
|
||||
(await ChannelSubscriptionTable.count({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
})) > 0
|
||||
);
|
||||
}
|
||||
|
||||
async subscriptions(): Promise<ChannelSubscriptionModel[]> {
|
||||
const subscriptions = await UserTable.findAll({
|
||||
where: {
|
||||
userId: this.user.id
|
||||
userId: this.data.id
|
||||
}
|
||||
});
|
||||
|
||||
@@ -52,7 +82,11 @@ export type CreateUser = {
|
||||
hash: string;
|
||||
salt: string;
|
||||
};
|
||||
subscriptionPasswordSalt: string;
|
||||
decryptionKeySalt: string;
|
||||
masterKey: {
|
||||
cipher: string;
|
||||
nonce: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function createUser(user: CreateUser): Promise<User> {
|
||||
@@ -64,7 +98,9 @@ export async function createUser(user: CreateUser): Promise<User> {
|
||||
passwordHash: user.password.hash,
|
||||
passwordSalt: user.password.salt,
|
||||
created: new Date(),
|
||||
subscriptionPasswordSalt: user.subscriptionPasswordSalt
|
||||
decryptionKeySalt: user.decryptionKeySalt,
|
||||
masterKeyCipher: user.masterKey.cipher,
|
||||
masterKeyNonce: user.masterKey.nonce
|
||||
};
|
||||
|
||||
let userCreated = false;
|
||||
|
||||
@@ -9,7 +9,11 @@ const zUserCreate = z.object({
|
||||
hash: z.string().max(255),
|
||||
salt: z.string().max(255)
|
||||
}),
|
||||
subscriptionPasswordSalt: z.string().max(255)
|
||||
decryptionKeySalt: z.string().max(255),
|
||||
masterKey: z.object({
|
||||
cipher: z.string().max(255),
|
||||
nonce: z.string().max(255)
|
||||
})
|
||||
});
|
||||
|
||||
export async function POST({ request }) {
|
||||
@@ -27,7 +31,11 @@ export async function POST({ request }) {
|
||||
hash: userToCreate.data.password.hash,
|
||||
salt: userToCreate.data.password.salt
|
||||
},
|
||||
subscriptionPasswordSalt: userToCreate.data.subscriptionPasswordSalt
|
||||
decryptionKeySalt: userToCreate.data.decryptionKeySalt,
|
||||
masterKey: {
|
||||
cipher: userToCreate.data.masterKey.cipher,
|
||||
nonce: userToCreate.data.masterKey.nonce
|
||||
}
|
||||
});
|
||||
|
||||
return new Response('');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { authenticateUser } from '$lib/server/user';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import z from 'zod';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { sign } from 'cookie-signature';
|
||||
@@ -27,5 +27,8 @@ export async function POST({ request, cookies }) {
|
||||
maxAge: 60 * 60 * 24 * 60 // 60 days
|
||||
});
|
||||
|
||||
return new Response();
|
||||
return json({
|
||||
masterKeyCipher: userModel.data.masterKeyCipher,
|
||||
masterKeyNonce: userModel.data.masterKeyNonce
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user