mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-20 18:30:15 +01:00
feat: started with proper E2EE ;)
This commit is contained in:
parent
e3b5c59949
commit
18d019d8b3
13 changed files with 380 additions and 1 deletions
|
|
@ -54,6 +54,11 @@ const messageSchema = mongoose.Schema(
|
||||||
type: String,
|
type: String,
|
||||||
meiliIndex: true,
|
meiliIndex: true,
|
||||||
},
|
},
|
||||||
|
// If the message is encrypted (and stored in 'text'),
|
||||||
|
// then this field should hold the IV used during encryption.
|
||||||
|
messageEncryptionIV: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
summary: {
|
summary: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,13 @@ const shareSchema = mongoose.Schema(
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
// --- Field for re-encrypting the conversation key for the forked user ---
|
||||||
|
encryptionKeys: [
|
||||||
|
{
|
||||||
|
user: { type: String, index: true },
|
||||||
|
encryptedKey: { type: String },
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{ timestamps: true },
|
{ timestamps: true },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ const { SystemRoles } = require('librechat-data-provider');
|
||||||
* @property {Array} [plugins=[]] - List of plugins used by the user
|
* @property {Array} [plugins=[]] - List of plugins used by the user
|
||||||
* @property {Array.<MongoSession>} [refreshToken] - List of sessions with refresh tokens
|
* @property {Array.<MongoSession>} [refreshToken] - List of sessions with refresh tokens
|
||||||
* @property {Date} [expiresAt] - Optional expiration date of the file
|
* @property {Date} [expiresAt] - Optional expiration date of the file
|
||||||
|
* @property {string} [encryptionPublicKey] - The user's public key for E2EE (client-generated)
|
||||||
|
* @property {string} [encryptedPrivateKey] - The user's private key encrypted with a user-defined passphrase
|
||||||
|
* @property {string} [encryptionSalt] - The salt used for PBKDF2 during encryption
|
||||||
|
* @property {string} [encryptionIV] - The initialization vector used for encryption (AES-GCM)
|
||||||
* @property {Date} [createdAt] - Date when the user was created (added by timestamps)
|
* @property {Date} [createdAt] - Date when the user was created (added by timestamps)
|
||||||
* @property {Date} [updatedAt] - Date when the user was last updated (added by timestamps)
|
* @property {Date} [updatedAt] - Date when the user was last updated (added by timestamps)
|
||||||
*/
|
*/
|
||||||
|
|
@ -132,6 +136,27 @@ const userSchema = mongoose.Schema(
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
// --- New Fields for E2EE ---
|
||||||
|
encryptionPublicKey: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
// Provided by the client after key generation.
|
||||||
|
},
|
||||||
|
encryptedPrivateKey: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
// The private key encrypted on the client with the user’s encryption passphrase.
|
||||||
|
},
|
||||||
|
encryptionSalt: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
// Salt used for PBKDF2 when encrypting the private key.
|
||||||
|
},
|
||||||
|
encryptionIV: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
// IV used for AES-GCM encryption of the private key.
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{ timestamps: true },
|
{ timestamps: true },
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const {
|
||||||
deleteMessages,
|
deleteMessages,
|
||||||
deleteUserById,
|
deleteUserById,
|
||||||
deleteAllUserSessions,
|
deleteAllUserSessions,
|
||||||
|
updateUser,
|
||||||
} = require('~/models');
|
} = require('~/models');
|
||||||
const User = require('~/models/User');
|
const User = require('~/models/User');
|
||||||
const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService');
|
const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService');
|
||||||
|
|
@ -162,6 +163,34 @@ const resendVerificationController = async (req, res) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateUserEncryptionController = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { encryptionPublicKey, encryptedPrivateKey, encryptionSalt, encryptionIV } = req.body;
|
||||||
|
|
||||||
|
// Validate required parameters
|
||||||
|
if (!encryptionPublicKey || !encryptedPrivateKey || !encryptionSalt || !encryptionIV) {
|
||||||
|
return res.status(400).json({ message: 'Missing encryption parameters.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the helper function to update the user.
|
||||||
|
const updatedUser = await updateUser(req.user.id, {
|
||||||
|
encryptionPublicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptionSalt,
|
||||||
|
encryptionIV,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updatedUser) {
|
||||||
|
return res.status(404).json({ message: 'User not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[updateUserEncryptionController]', error);
|
||||||
|
res.status(500).json({ message: 'Something went wrong updating encryption keys.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getUserController,
|
getUserController,
|
||||||
getTermsStatusController,
|
getTermsStatusController,
|
||||||
|
|
@ -170,4 +199,5 @@ module.exports = {
|
||||||
verifyEmailController,
|
verifyEmailController,
|
||||||
updateUserPluginsController,
|
updateUserPluginsController,
|
||||||
resendVerificationController,
|
resendVerificationController,
|
||||||
|
updateUserEncryptionController,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,14 @@ const {
|
||||||
resendVerificationController,
|
resendVerificationController,
|
||||||
getTermsStatusController,
|
getTermsStatusController,
|
||||||
acceptTermsController,
|
acceptTermsController,
|
||||||
|
updateUserEncryptionController,
|
||||||
} = require('~/server/controllers/UserController');
|
} = require('~/server/controllers/UserController');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/', requireJwtAuth, getUserController);
|
router.get('/', requireJwtAuth, getUserController);
|
||||||
router.get('/terms', requireJwtAuth, getTermsStatusController);
|
router.get('/terms', requireJwtAuth, getTermsStatusController);
|
||||||
|
router.put('/encryption', requireJwtAuth, updateUserEncryptionController);
|
||||||
router.post('/terms/accept', requireJwtAuth, acceptTermsController);
|
router.post('/terms/accept', requireJwtAuth, acceptTermsController);
|
||||||
router.post('/plugins', requireJwtAuth, updateUserPluginsController);
|
router.post('/plugins', requireJwtAuth, updateUserPluginsController);
|
||||||
router.delete('/delete', requireJwtAuth, canDeleteAccount, deleteUserController);
|
router.delete('/delete', requireJwtAuth, canDeleteAccount, deleteUserController);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import EncryptionPassphrase from './EncryptionPassphrase';
|
||||||
import MaximizeChatSpace from './MaximizeChatSpace';
|
import MaximizeChatSpace from './MaximizeChatSpace';
|
||||||
import FontSizeSelector from './FontSizeSelector';
|
import FontSizeSelector from './FontSizeSelector';
|
||||||
import SendMessageKeyEnter from './EnterToSend';
|
import SendMessageKeyEnter from './EnterToSend';
|
||||||
|
|
@ -35,6 +36,9 @@ function Chat() {
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<ScrollButton />
|
<ScrollButton />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="pb-3">
|
||||||
|
<EncryptionPassphrase />
|
||||||
|
</div>
|
||||||
<ForkSettings />
|
<ForkSettings />
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<ModularChat />
|
<ModularChat />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
import React, { useState, ChangeEvent, FC } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
OGDialog,
|
||||||
|
OGDialogContent,
|
||||||
|
OGDialogHeader,
|
||||||
|
OGDialogTitle,
|
||||||
|
Input,
|
||||||
|
} from '~/components';
|
||||||
|
import { Lock, Key } from 'lucide-react';
|
||||||
|
import { useAuthContext, useLocalize } from '~/hooks';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import store from '~/store';
|
||||||
|
import type { TUser } from 'librechat-data-provider';
|
||||||
|
import { useToastContext } from '~/Providers';
|
||||||
|
import { useSetUserEncryptionMutation } from '~/data-provider';
|
||||||
|
|
||||||
|
// Helper: Convert a Base64 string to Uint8Array.
|
||||||
|
const base64ToUint8Array = (base64: string): Uint8Array => {
|
||||||
|
const binaryString = window.atob(base64);
|
||||||
|
const len = binaryString.length;
|
||||||
|
const bytes = new Uint8Array(len);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: Convert a Uint8Array to a hex string (for debugging).
|
||||||
|
const uint8ArrayToHex = (array: Uint8Array): string =>
|
||||||
|
Array.from(array)
|
||||||
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
// Derive an AES-GCM key from the passphrase using PBKDF2.
|
||||||
|
const deriveKey = async (passphrase: string, salt: Uint8Array): Promise<CryptoKey> => {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const keyMaterial = await window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
encoder.encode(passphrase),
|
||||||
|
'PBKDF2',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
const derivedKey = await window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt,
|
||||||
|
iterations: 100000, // Adjust as needed for security/performance trade-off
|
||||||
|
hash: 'SHA-256',
|
||||||
|
},
|
||||||
|
keyMaterial,
|
||||||
|
{ name: 'AES-GCM', length: 256 },
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
// Debug: export the derived key and log it.
|
||||||
|
const rawKey = await window.crypto.subtle.exportKey('raw', derivedKey);
|
||||||
|
console.debug('Derived key (hex):', uint8ArrayToHex(new Uint8Array(rawKey)));
|
||||||
|
return derivedKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decrypt the encrypted private key using the derived key and IV.
|
||||||
|
const decryptPrivateKey = async (
|
||||||
|
derivedKey: CryptoKey,
|
||||||
|
iv: Uint8Array,
|
||||||
|
ciphertextBase64: string
|
||||||
|
): Promise<string> => {
|
||||||
|
const ciphertext = base64ToUint8Array(ciphertextBase64);
|
||||||
|
const decryptedBuffer = await window.crypto.subtle.decrypt(
|
||||||
|
{ name: 'AES-GCM', iv },
|
||||||
|
derivedKey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
return decoder.decode(decryptedBuffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserKeysSettings: FC = () => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { user } = useAuthContext();
|
||||||
|
const setUser = useSetRecoilState(store.user);
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
const [passphrase, setPassphrase] = useState<string>('');
|
||||||
|
|
||||||
|
// Mutation hook for updating user encryption keys.
|
||||||
|
const { mutateAsync: setEncryption } = useSetUserEncryptionMutation({
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Error updating encryption keys:', error);
|
||||||
|
showToast({ message: localize('com_ui_upload_error'), status: 'error' });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activation/Update flow: Generate new keys and update user encryption fields.
|
||||||
|
const activateEncryption = async (): Promise<{
|
||||||
|
encryptionPublicKey: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
encryptionSalt: string;
|
||||||
|
encryptionIV: string;
|
||||||
|
} | void> => {
|
||||||
|
if (!passphrase) {
|
||||||
|
console.error('Passphrase is empty.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
console.error('User object is missing.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.debug('[Debug] Activating E2EE encryption...');
|
||||||
|
|
||||||
|
// Generate a new RSA-OAEP key pair.
|
||||||
|
const keyPair = await window.crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: 'RSA-OAEP',
|
||||||
|
modulusLength: 2048,
|
||||||
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
hash: 'SHA-256',
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
// Export public and private keys.
|
||||||
|
const publicKeyBuffer = await window.crypto.subtle.exportKey('spki', keyPair.publicKey);
|
||||||
|
const privateKeyBuffer = await window.crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
|
||||||
|
const publicKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(publicKeyBuffer)));
|
||||||
|
const privateKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(privateKeyBuffer)));
|
||||||
|
console.debug('New public key:', publicKeyBase64);
|
||||||
|
console.debug('New private key (plaintext):', privateKeyBase64);
|
||||||
|
|
||||||
|
// Generate a salt and IV.
|
||||||
|
const salt = window.crypto.getRandomValues(new Uint8Array(16)); // 16 bytes salt
|
||||||
|
const iv = window.crypto.getRandomValues(new Uint8Array(12)); // 12 bytes IV
|
||||||
|
|
||||||
|
// Derive a symmetric key from the passphrase.
|
||||||
|
const derivedKey = await deriveKey(passphrase, salt);
|
||||||
|
|
||||||
|
// Encrypt the private key.
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const privateKeyBytes = encoder.encode(privateKeyBase64);
|
||||||
|
const encryptedPrivateKeyBuffer = await window.crypto.subtle.encrypt(
|
||||||
|
{ name: 'AES-GCM', iv },
|
||||||
|
derivedKey,
|
||||||
|
privateKeyBytes
|
||||||
|
);
|
||||||
|
const encryptedPrivateKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(encryptedPrivateKeyBuffer)));
|
||||||
|
|
||||||
|
// Convert salt and IV to Base64.
|
||||||
|
const saltBase64 = btoa(String.fromCharCode(...salt));
|
||||||
|
const ivBase64 = btoa(String.fromCharCode(...iv));
|
||||||
|
|
||||||
|
console.debug('Activation complete:');
|
||||||
|
console.debug('Encrypted private key:', encryptedPrivateKeyBase64);
|
||||||
|
console.debug('Salt (base64):', saltBase64);
|
||||||
|
console.debug('IV (base64):', ivBase64);
|
||||||
|
|
||||||
|
return {
|
||||||
|
encryptionPublicKey: publicKeyBase64,
|
||||||
|
encryptedPrivateKey: encryptedPrivateKeyBase64,
|
||||||
|
encryptionSalt: saltBase64,
|
||||||
|
encryptionIV: ivBase64,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during activation:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (): Promise<void> => {
|
||||||
|
// Activate encryption (or re-activate) by generating new keys.
|
||||||
|
const newEncryption = await activateEncryption();
|
||||||
|
if (newEncryption) {
|
||||||
|
try {
|
||||||
|
// Call the mutation to update the backend.
|
||||||
|
await setEncryption(newEncryption);
|
||||||
|
showToast({ message: localize('com_ui_upload_success') });
|
||||||
|
// Update local user state with the new encryption keys.
|
||||||
|
setUser((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...newEncryption,
|
||||||
|
}) as TUser);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Mutation error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setDialogOpen(false);
|
||||||
|
setPassphrase('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setPassphrase(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* List item style */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Key className="flex w-[20px] h-[20px]" />
|
||||||
|
<span id="user-keys-label">
|
||||||
|
{localize('com_nav_chat_encryption_settings')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
aria-label="Set/Change encryption keys"
|
||||||
|
onClick={() => setDialogOpen(true)}
|
||||||
|
data-testid="userKeysSettings"
|
||||||
|
>
|
||||||
|
<Lock className="mr-2 flex w-[22px] items-center stroke-1" />
|
||||||
|
<span>{localize('com_nav_chat_change_passphrase')}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Optionally display current public key */}
|
||||||
|
{user?.encryptionPublicKey && (
|
||||||
|
<div className="pt-2 text-xs text-gray-500">
|
||||||
|
{localize('com_nav_chat_current_public_key')}: {user.encryptionPublicKey.slice(0, 30)}...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dialog for setting/updating keys */}
|
||||||
|
<OGDialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
|
<OGDialogContent className="w-11/12 max-w-sm" style={{ borderRadius: '12px' }}>
|
||||||
|
<OGDialogHeader>
|
||||||
|
<OGDialogTitle>
|
||||||
|
{localize('com_nav_chat_enter_your_passphrase')}
|
||||||
|
</OGDialogTitle>
|
||||||
|
</OGDialogHeader>
|
||||||
|
<div className="p-4 flex flex-col gap-4">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={passphrase}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder={localize('com_nav_chat_passphrase_placeholder')}
|
||||||
|
aria-label={localize('com_nav_chat_enter_your_passphrase')}
|
||||||
|
/>
|
||||||
|
<Button variant="outline" onClick={handleSubmit}>
|
||||||
|
{localize('com_ui_submit')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</OGDialogContent>
|
||||||
|
</OGDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserKeysSettings;
|
||||||
|
|
@ -897,7 +897,7 @@ export const useUploadAssistantAvatarMutation = (
|
||||||
unknown // context
|
unknown // context
|
||||||
> => {
|
> => {
|
||||||
return useMutation([MutationKeys.assistantAvatarUpload], {
|
return useMutation([MutationKeys.assistantAvatarUpload], {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
mutationFn: ({ postCreation, ...variables }: t.AssistantAvatarVariables) =>
|
mutationFn: ({ postCreation, ...variables }: t.AssistantAvatarVariables) =>
|
||||||
dataService.uploadAssistantAvatar(variables),
|
dataService.uploadAssistantAvatar(variables),
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
|
|
@ -1068,3 +1068,24 @@ export const useAcceptTermsMutation = (
|
||||||
onMutate: options?.onMutate,
|
onMutate: options?.onMutate,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useSetUserEncryptionMutation = (
|
||||||
|
options?: {
|
||||||
|
onSuccess?: (
|
||||||
|
data: t.UpdateUserEncryptionResponse,
|
||||||
|
variables: t.UpdateUserEncryptionRequest,
|
||||||
|
context?: unknown
|
||||||
|
) => void;
|
||||||
|
onError?: (
|
||||||
|
error: unknown,
|
||||||
|
variables: t.UpdateUserEncryptionRequest,
|
||||||
|
context?: unknown
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
): UseMutationResult<t.UpdateUserEncryptionResponse, unknown, t.UpdateUserEncryptionRequest, unknown> => {
|
||||||
|
return useMutation([MutationKeys.updateUserEncryption], {
|
||||||
|
mutationFn: (variables: t.UpdateUserEncryptionRequest) =>
|
||||||
|
dataService.updateUserEncryption(variables),
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -531,6 +531,11 @@
|
||||||
"com_ui_controls": "Controls",
|
"com_ui_controls": "Controls",
|
||||||
"com_ui_copied": "Copied!",
|
"com_ui_copied": "Copied!",
|
||||||
"com_ui_copied_to_clipboard": "Copied to clipboard",
|
"com_ui_copied_to_clipboard": "Copied to clipboard",
|
||||||
|
"com_nav_chat_encryption_settings": "Encryption Settings",
|
||||||
|
"com_nav_chat_change_passphrase": "Change Passphrase",
|
||||||
|
"com_nav_chat_enter_your_passphrase": "Enter your passphrase",
|
||||||
|
"com_nav_chat_passphrase_placeholder": "Type your encryption passphrase here...",
|
||||||
|
"com_nav_chat_current_public_key": "Current Public Key",
|
||||||
"com_ui_copy_code": "Copy code",
|
"com_ui_copy_code": "Copy code",
|
||||||
"com_ui_copy_link": "Copy link",
|
"com_ui_copy_link": "Copy link",
|
||||||
"com_ui_copy_to_clipboard": "Copy to clipboard",
|
"com_ui_copy_to_clipboard": "Copy to clipboard",
|
||||||
|
|
|
||||||
|
|
@ -237,3 +237,5 @@ export const addTagToConversation = (conversationId: string) =>
|
||||||
export const userTerms = () => '/api/user/terms';
|
export const userTerms = () => '/api/user/terms';
|
||||||
export const acceptUserTerms = () => '/api/user/terms/accept';
|
export const acceptUserTerms = () => '/api/user/terms/accept';
|
||||||
export const banner = () => '/api/banner';
|
export const banner = () => '/api/banner';
|
||||||
|
|
||||||
|
export const encryption = () => '/api/user/encryption';
|
||||||
|
|
|
||||||
|
|
@ -774,3 +774,9 @@ export function acceptTerms(): Promise<t.TAcceptTermsResponse> {
|
||||||
export function getBanner(): Promise<t.TBannerResponse> {
|
export function getBanner(): Promise<t.TBannerResponse> {
|
||||||
return request.get(endpoints.banner());
|
return request.get(endpoints.banner());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const updateUserEncryption = (
|
||||||
|
payload: t.UpdateUserEncryptionRequest,
|
||||||
|
): Promise<t.UpdateUserEncryptionResponse> => {
|
||||||
|
return request.put(endpoints.encryption(), payload);
|
||||||
|
};
|
||||||
|
|
@ -67,4 +67,5 @@ export enum MutationKeys {
|
||||||
deleteAgentAction = 'deleteAgentAction',
|
deleteAgentAction = 'deleteAgentAction',
|
||||||
deleteUser = 'deleteUser',
|
deleteUser = 'deleteUser',
|
||||||
updateRole = 'updateRole',
|
updateRole = 'updateRole',
|
||||||
|
updateUserEncryption = 'updateUserEncryption',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,10 @@ export type TUser = {
|
||||||
plugins?: string[];
|
plugins?: string[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
encryptionPublicKey?: string;
|
||||||
|
encryptedPrivateKey?: string; // Encrypted as a Base64 string
|
||||||
|
encryptionSalt?: string; // Base64 encoded salt used for PBKDF2
|
||||||
|
encryptionIV?: string; // Base64 encoded IV for AES-GCM
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetConversationsResponse = {
|
export type TGetConversationsResponse = {
|
||||||
|
|
@ -473,3 +477,21 @@ export type TAcceptTermsResponse = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TBannerResponse = TBanner | null;
|
export type TBannerResponse = TBanner | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request type for updating user encryption keys.
|
||||||
|
*/
|
||||||
|
export type UpdateUserEncryptionRequest = {
|
||||||
|
encryptionPublicKey: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
encryptionSalt: string;
|
||||||
|
encryptionIV: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response type for updating user encryption keys.
|
||||||
|
*/
|
||||||
|
export type UpdateUserEncryptionResponse = {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue