Introduced master key system

This commit is contained in:
WardPearce
2026-02-14 02:13:50 +13:00
parent f84e973bea
commit 4b8864bb57
5 changed files with 153 additions and 38 deletions
+69 -17
View File
@@ -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;
}
+22 -6
View File
@@ -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
},
+47 -11
View File
@@ -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
});
}