full
This commit is contained in:
162
controllers/application/account/e2eeController.js
Normal file
162
controllers/application/account/e2eeController.js
Normal file
@@ -0,0 +1,162 @@
|
||||
const mongoose = require('mongoose')
|
||||
const UserModel = require('../../../models/UserModel')
|
||||
const DeviceLinkSessionModel = require('../../../models/DeviceLinkSessionModel')
|
||||
|
||||
const DEVICE_LINK_TTL_MS = 5 * 60 * 1000
|
||||
|
||||
function findOwnSession (linkId, userId) {
|
||||
if (!mongoose.Types.ObjectId.isValid(linkId)) return null
|
||||
return DeviceLinkSessionModel.findOne({ _id: linkId, user: userId })
|
||||
}
|
||||
|
||||
const putMyPublicKey = async (req, res, next) => {
|
||||
try {
|
||||
const userId = String(req.user._id)
|
||||
const { publicKey } = req.body
|
||||
|
||||
if (!publicKey || typeof publicKey !== 'string' || publicKey.length < 32 || publicKey.length > 4096) {
|
||||
return res.status(422).json({ error: 'publicKey is required' })
|
||||
}
|
||||
|
||||
const trimmed = String(publicKey).trim()
|
||||
const existing = await UserModel.findById(userId).select('e2ee_public_key').lean()
|
||||
|
||||
// کلید عمومی همیشه یکسان میمونه مگر کاربر واقعاً هویتِ E2EEش رو عوض کنه (دستگاه
|
||||
// جدید بدون لینککردن، پاکشدن دیتای اپ و...). اگه از قبل یه کلید ثبتشده و مقدار
|
||||
// تازه باهاش فرق داره، یعنی یا کلاینت واقعاً هویتش رو عوض کرده یا یه باگ کلاینتی
|
||||
// (مثلاً race توی ساختن کلید) داره جایگزینش میکنه — فقط لاگ میکنیم تا قابلردیابی
|
||||
// باشه، بدون اینکه خودِ درخواست رو رد کنیم (چون سرور نمیتونه تشخیص بده کدوم درسته)
|
||||
if (existing?.e2ee_public_key && existing.e2ee_public_key !== trimmed) {
|
||||
console.warn(`[e2ee] public key rotated for user ${userId} (${existing.e2ee_public_key.length} -> ${trimmed.length} chars)`)
|
||||
}
|
||||
|
||||
if (existing?.e2ee_public_key !== trimmed) {
|
||||
await UserModel.findByIdAndUpdate(userId, { e2ee_public_key: trimmed })
|
||||
}
|
||||
|
||||
res.json({ ok: true })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
const getPublicKeyByUserId = async (req, res, next) => {
|
||||
try {
|
||||
const { userId } = req.params
|
||||
if (!userId) {
|
||||
return res.status(400).json({ error: 'userId is required' })
|
||||
}
|
||||
|
||||
const user = await UserModel.findById(userId).select('e2ee_public_key').lean()
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' })
|
||||
}
|
||||
|
||||
res.json({
|
||||
userId: String(user._id),
|
||||
publicKey: user.e2ee_public_key || null
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* New device (no E2EE identity yet) starts a link session with its own ephemeral
|
||||
* ECDH public key. Returns a linkId to encode as a QR — the already-linked device
|
||||
* scans it, wraps its identity private key against this ephemeral key, and posts
|
||||
* it back via `approveDeviceLink`. Server only ever stores/relays ciphertext.
|
||||
*/
|
||||
const startDeviceLink = async (req, res, next) => {
|
||||
try {
|
||||
const { ephemeralPublicKey } = req.body
|
||||
if (
|
||||
!ephemeralPublicKey ||
|
||||
typeof ephemeralPublicKey !== 'string' ||
|
||||
ephemeralPublicKey.length < 32 ||
|
||||
ephemeralPublicKey.length > 2048
|
||||
) {
|
||||
return res.status(422).json({ error: 'ephemeralPublicKey is required' })
|
||||
}
|
||||
|
||||
const session = await DeviceLinkSessionModel.create({
|
||||
user: req.user._id,
|
||||
ephemeralPublicKeyB64: ephemeralPublicKey.trim(),
|
||||
expiresAt: new Date(Date.now() + DEVICE_LINK_TTL_MS)
|
||||
})
|
||||
|
||||
res.json({ linkId: String(session._id), expiresAt: session.expiresAt })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** Already-linked device fetches the new device's ephemeral public key to wrap the identity key against. */
|
||||
const getDeviceLinkSession = async (req, res, next) => {
|
||||
try {
|
||||
const session = await findOwnSession(req.params.linkId, req.user._id)
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Link not found or expired' })
|
||||
}
|
||||
|
||||
res.json({
|
||||
ephemeralPublicKey: session.ephemeralPublicKeyB64,
|
||||
status: session.status
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** Already-linked device submits the identity private key wrapped for the new device's ephemeral key. */
|
||||
const approveDeviceLink = async (req, res, next) => {
|
||||
try {
|
||||
const { wrappedIdentityKey } = req.body
|
||||
if (!wrappedIdentityKey || typeof wrappedIdentityKey !== 'string') {
|
||||
return res.status(422).json({ error: 'wrappedIdentityKey is required' })
|
||||
}
|
||||
|
||||
const session = await findOwnSession(req.params.linkId, req.user._id)
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Link not found or expired' })
|
||||
}
|
||||
|
||||
session.wrappedIdentityKey = wrappedIdentityKey
|
||||
session.status = 'fulfilled'
|
||||
await session.save()
|
||||
|
||||
res.json({ ok: true })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** New device polls until the wrapped key arrives. Single-use: the session is deleted once consumed. */
|
||||
const pollDeviceLink = async (req, res, next) => {
|
||||
try {
|
||||
const session = await findOwnSession(req.params.linkId, req.user._id)
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Link not found or expired' })
|
||||
}
|
||||
|
||||
if (session.status !== 'fulfilled' || !session.wrappedIdentityKey) {
|
||||
return res.json({ status: 'pending' })
|
||||
}
|
||||
|
||||
const wrappedIdentityKey = session.wrappedIdentityKey
|
||||
await DeviceLinkSessionModel.deleteOne({ _id: session._id })
|
||||
|
||||
res.json({ status: 'fulfilled', wrappedIdentityKey })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
putMyPublicKey,
|
||||
getPublicKeyByUserId,
|
||||
startDeviceLink,
|
||||
getDeviceLinkSession,
|
||||
approveDeviceLink,
|
||||
pollDeviceLink
|
||||
}
|
||||
Reference in New Issue
Block a user