دفتر

نِکونیموس؛ گربه‌ای که قرار بود فقط پیام را برساند

روایت بازسازی نکونیموس؛ از تجربه ربات‌های پیام ناشناس و خطر آرشیو داده تا sealed ticket، دیتابیس کور، پیشنهاد گفت‌وگو و مرزهای واقعی حریم خصوصی.

نویسنده Ali Zemaniبه‌روزرسانی ۱۰ دقیقه مطالعه
نِکونیموس؛ گربه‌ای که قرار بود فقط پیام را برساند

ایده‌ی اولیه‌ی ساخت نِکونیموس چند سال پیش توی ذهنم شکل گرفت، اما چیزی که آن را جدی‌تر کرد ماجرای هک‌شدن چند ربات معروف پیام ناشناس بود.

بعد از دسترسی به سرورهای این بات‌ها مشخص شد که چیزی که کاربران صرفاً به‌عنوان یک واسطه برای فرستادن پیام ناشناس می‌دیدند، در پشت صحنه حجم خیلی بزرگی از پیام‌ها، عکس‌ها، ویدیوها و اطلاعات میلیون‌ها کاربر را نگه داشته است.

جزئیات روایت‌هایی که آن زمان منتشر می‌شد یکسان نبود؛ اینکه چه داده‌هایی پاک شد، چه بات‌هایی از کار افتادند و بعدش دقیقاً چه اتفاقی افتاد.

ولی خب، شد آنچه شد:))

چیزی که برای من مهم بود، اصل ماجرا بود:

یک ربات پیام ناشناس قرار بود فقط پیام را از یک نفر بگیرد و به یک نفر دیگر برساند.

پس چرا باید پشت صحنه تبدیل به یک آرشیو بزرگ و منسجم از زندگی خصوصی آدم‌ها شود؟

ایده‌ی من این بود که یک ربات پیام ناشناس در تلگرام بسازم که فقط و فقط همان کاری را انجام دهد که باید انجام دهد.

نه بیشتر.

پیام را بگیرد، به مقصد برساند و تا جای ممکن هیچ داده‌ی اضافه‌ای نگه ندارد.

اگر کسی هم دیگر نمی‌خواست از بات استفاده کند، بتواند حسابش را پاک کند، لینک قبلی‌اش از کار بیفتد و داده‌های متصل به حسابش از سیستم حذف شوند.

بخش پاک‌کردن حساب خیلی پیچیده نبود.

بخش اصلی سؤال دیگری بود:

چطور داده‌ها را در کمترین و محدودترین حالت ممکن نگه داریم، ولی بات هنوز بتواند پیام ناشناس، پاسخ ناشناس، مسدودسازی، گزارش و ادامه‌ی یک گفت‌وگو را درست انجام دهد؟

مسئله فقط رمزنگاری متن پیام نبود

اولین راهی که به ذهن می‌رسد این است که متن پیام‌ها را رمزنگاری کنیم و داخل دیتابیس بگذاریم.

ولی اگر کنار آن هنوز چنین اطلاعاتی داشته باشیم:

این پیام را کاربر A فرستاده
برای کاربر B
داخل گفت‌وگوی C
در زمان D

عملاً بخش مهمی از حریم خصوصی از بین رفته است.

حتی اگر متن پیام خوانده نشود، خود ساختار دیتابیس می‌تواند نشان دهد چه کسی با چه کسی در ارتباط بوده، چند بار پیام فرستاده و هر ارتباط چقدر ادامه پیدا کرده است.

پس مسئله فقط مخفی‌کردن متن نبود.

باید تا جای ممکن خود رابطه هم از شکل مستقیم و قابل جست‌وجو خارج می‌شد.

با همین فرض، هسته‌ی اولیه‌ی تیکتینگ نِکونیموس را طراحی کردم.

به‌جای اینکه هر پیام یک ردیف معمولی متصل به فرستنده و گیرنده باشد، هر پیام تبدیل شد به یک تیکت مستقل.

چیزی شبیه یک پاکت مهروموم‌شده که فقط اطلاعات لازم برای رساندن همان پیام و انجام actionهای مربوط به خودش را درونش دارد.

مدل اولیه ترکیبی بود از:

Key-Value Storage
+ Blind Hashes
+ Encrypted Payload
+ Short-lived Capabilities

هر پیام برای مدت محدود رمزنگاری می‌شد و یک capability مستقل برای آن ساخته می‌شد.

بخشی از این capability برای پیدا کردن رکورد کور داخل vault استفاده می‌شود و بخش دیگر برای مشتق‌کردن کلیدهای رمزنگاری لازم است.

خود capability به شکل خام داخل TicketVault ذخیره نمی‌شود.

دیتابیس فقط یک lookup هش‌شده، اطلاعات route رمزنگاری‌شده، payload رمزنگاری‌شده و metadata محدود را می‌بیند.

حتی این capability هم به‌تنهایی برای بازکردن تیکت کافی نیست؛ سیستم بررسی می‌کند که action توسط همان حساب فعلی گیرنده انجام می‌شود.

وقتی گیرنده inbox خودش را باز می‌کند، بات تیکت را پیدا می‌کند، مالکیت آن را بررسی می‌کند، payload را برای همان تحویل باز می‌کند و پیام را به تلگرام می‌فرستد.

بعد از تحویل موفق، payload از حافظه‌ی نِکو پاک می‌شود و آیتم unread هم دیگر در inbox باقی نمی‌ماند.

چیزی که باقی می‌ماند فقط route رمزنگاری‌شده‌ی حداقلی است تا دکمه‌های همان پیام بتوانند برای مدت محدود کار کنند:

  • پاسخ دادن؛
  • گذاشتن نام خصوصی؛
  • مسدود کردن؛
  • گزارش کردن.

یعنی inbox نِکو یک آرشیو پیام نیست.

یک صف محدود برای تحویل پیام‌هایی است که هنوز دیده نشده‌اند.

بعد از تحویل، متن پیام داخل تلگرام کاربر قرار دارد و نِکو دیگر لازم نیست یک نسخه‌ی اضافه از آن را برای خودش نگه دارد.

یک دیتابیس عمداً کور

در این مدل، نِکو تا جای ممکن از نگه‌داشتن یک جدول مستقیم از روابط کاربران دوری می‌کند.

D1 اطلاعات ساختاری مثل حساب‌های فعال، لینک‌های عمومی و آمار تجمیعی را نگه می‌دارد؛ نه متن پیام‌های ناشناس و نه یک graph مستقیم از فرستنده و گیرنده.

KV فقط cache و routing است و منبع اصلی داده نیست.

TicketVault تیکت‌های رمزنگاری‌شده را نگه می‌دارد، بدون اینکه داخل هر رکورد sender_id یا recipient_id مستقیمی وجود داشته باشد.

گزارش‌ها نیز با tagهای کور ساخته می‌شوند تا سیستم بتواند الگوهای سوءاستفاده را تشخیص دهد، بدون اینکه برای این کار یک شبکه‌ی خوانا از کاربران بسازد.

این طراحی قرار نیست بگوید دیتابیس هیچ چیزی برای افشا ندارد.

زمان‌ها، تعداد رکوردها، وضعیت‌ها، اندازه‌ی ciphertextها و الگوهای دسترسی همچنان metadata هستند.

اگر runtime یا کلیدهای اصلی برنامه در اختیار مهاجم قرار بگیرند، رمزنگاری در حالت ذخیره‌شده هم دیگر محافظت کامل ایجاد نمی‌کند.

ولی فرق بزرگی وجود دارد میان دیتابیسی که از اول یک جدول آماده‌ی «چه کسی به چه کسی چه گفت» ساخته، با سیستمی که عمداً داده‌ها را میان چند مرز جدا، با lookupهای کور و capsuleهای رمزنگاری‌شده پخش کرده است.

هدف نِکو ناشناسی جادویی نبود.

هدف این بود که حتی خود سیستم هم بیشتر از چیزی که برای انجام وظیفه‌اش لازم دارد، درباره‌ی ارتباط کاربران نداند.

بازسازی نِکو

در بازسازی ژوئیه‌ی ۲۰۲۶، نِکو را از آن ایده‌ی اولیه‌ی خام بیرون آوردم و معماری، کد، امنیت، failure handling و تجربه‌ی کاربری‌اش را از نو ساختم.

نه اینکه فقط یک‌سری سرویس جدید Cloudflare را کنار هم بگذارم.

هر ابزار باید دقیقاً یک مسئولیت مشخص در هسته‌ی نِکو می‌داشت.

معماری فعلی یک Worker واحد دارد، ولی state و مسئولیت‌ها میان چند بخش جدا شده‌اند:

Cloudflare Worker + grammY
D1
Durable Objects
KV
Queues
Vectorize
Web Crypto

D1 فقط داده‌های ساختاری و آمار تجمیعی را نگه می‌دارد.

UserState وضعیت محلی هر کاربر، inbox موقت، draftها، blockها و rate limitها را مدیریت می‌کند.

TicketVault مسئول تیکت‌های ناشناس مهروموم‌شده است.

SafetyState گزارش‌های کور و sanctionهای مربوط به سوءاستفاده را نگه می‌دارد.

ProfileVault پروفایل‌های گفت‌وگو را به‌صورت رمزنگاری‌شده نگه می‌دارد.

ConversationVault و PairLedger پیشنهادها، درخواست‌ها، cooldownها و محدودیت‌های ارتباطی را بدون ساختن یک relationship table عمومی مدیریت می‌کنند.

TelegramOutbox ارسال پیام‌ها را به‌شکل paced و idempotent انجام می‌دهد تا retry یا تحویل چندباره‌ی Queue باعث ارسال تکراری نشود.

Queues کارهای پس‌زمینه مثل ارسال، آمار و به‌روزرسانی index را از مسیر اصلی کاربر جدا می‌کنند.

KV فقط برای cache و پیدا کردن سریع routeهاست.

Vectorize نیز فقط چند گزینه‌ی اولیه را پیدا می‌کند و هیچ تصمیم نهایی درباره آدم‌ها نمی‌گیرد.

این معماری میکروسرویس کلاسیک نیست.

یک سیستم Worker-native است که هر storage plane در آن دقیقاً می‌داند چه چیزی را باید نگه دارد و مهم‌تر از آن، چه چیزی را نباید نگه دارد.

پیشنهاد گفت‌وگو

در جریان بازسازی، تصمیم گرفتم یک بخش مهم دیگر هم به نِکو اضافه کنم:

پیشنهاد گفت‌وگو.

ایده این نبود که نِکو تبدیل به اپ دوستیابی، مچ‌یابی یا تست شخصیت شود.

ایده ساده‌تر بود:

آدم‌ها فقط بر اساس اینکه «به هم شبیه‌اند» لزوماً گفت‌وگوی خوبی ندارند.

گاهی یک نفر مستقیم‌تر حرف می‌زند، یک نفر آرام‌تر است، یکی پاسخ سریع می‌خواهد، یکی با فاصله فکر می‌کند، یکی دنبال گفت‌وگوی عمیق است و یکی ارتباط سبک‌تر را ترجیح می‌دهد.

برای همین یک ارزیابی ۲۵سؤالی با ۸ بُعد طراحی کردم که دو تصویر جدا می‌سازد:

من معمولاً چطور گفت‌وگو می‌کنم؟
از طرف مقابلم چه نوع گفت‌وگویی می‌خواهم؟

ورود به پیشنهادها کاملاً اختیاری است و نمایش کاربر از ابتدا خاموش می‌ماند.

در cleanup روز ۱۵ ژوئیه‌ی ۲۰۲۶، ارزیابی و پیشنهادها را از دو ابزار جدا به یک flow پیوسته‌تر تبدیل کردم. hub پیشنهاد گفت‌وگو حالا پیشرفت ارزیابی، وضعیت نمایش، آمادگی index و خلاصه‌ی کنترل‌شده‌ی پروفایل را کنار هم نشان می‌دهد.

۱۶ سؤال اول یک مقیاس ساده برای سبک خود کاربر دارند. ۸ سؤال بعدی به‌جای تکرار یک مقیاس مبهم، دو سر هر بُعد را روشن می‌کنند؛ مثلاً خیلی سبک تا خیلی عمیق، خیلی آرام تا خیلی سریع، یا خیلی غیرمستقیم تا خیلی مستقیم. پیشرفت ذخیره می‌شود و شروع دوباره نیز نمایش در پیشنهادها را خاموش می‌کند.

دکمه‌ی دیدن پیشنهادها فقط وقتی آماده می‌شود که هر دو vector پروفایل ساخته و index آن‌ها تأیید شده باشد. ممکن است خود پروفایل زودتر ذخیره شده باشد؛ در آن فاصله UI صادقانه می‌گوید هنوز در حال آماده‌شدن است.

Vectorize فقط تعداد محدودی گزینه‌ی اولیه را پیدا می‌کند.

رتبه‌بندی نهایی با الگوریتم deterministic در TypeScript انجام می‌شود؛ یعنی یک مدل هوش مصنوعی پشت صحنه تصمیم نمی‌گیرد که چه کسی برای چه کسی «مناسب» است.

سیستم فقط چند گزینه‌ی نزدیک‌تر بر اساس شرایط فعلی نشان می‌دهد.

بعد هم هیچ گفت‌وگویی خودکار شروع نمی‌شود.

یک نفر پیام شروع می‌نویسد، طرف مقابل آن را می‌پذیرد یا رد می‌کند و فقط در صورت پذیرش، همان درخواست به یک تیکت ناشناس معمولی تبدیل می‌شود.

یعنی حتی این بخش جدید هم در نهایت به همان هسته‌ی اصلی نِکو برمی‌گردد:

یک تیکت مستقل، یک مسیر محدود و یک گفت‌وگوی ناشناس بدون ساختن یک جدول مستقیم از رابطه‌ی دو کاربر.

گربه‌ی نارنجی پیام‌های ناشناس

بخش دیگری که برایم مهم بود، زبان خود بات بود.

اسم Nekonymous از ترکیب دو کلمه ساخته شده:

Neko
+
Anonymous

Neko در ژاپنی یعنی گربه و Anonymous هم یعنی ناشناس.

در نتیجه Nekonymous می‌تواند چیزی شبیه «گربه‌ی ناشناس» باشد.

یا دقیق‌تر:

گربه‌ی نارنجی پیام‌های ناشناس.

این اسم قرار نبود فقط روی لوگو بماند.

سعی کردم رفتار، کلمات، خطاها و حتی سردرگمی‌های بات هم شخصیت همین گربه را داشته باشند.

برای همین وقتی اولین بار بات را start می‌کنید، به‌جای یک متن رسمی و طولانی با این شروع روبه‌رو می‌شوید:

میو، رسیدی 🐾

من نِکونیموسم؛ گربه‌ی نارنجی پیام‌های ناشناس.

اینم لینک پیام ناشناس تو:

https://t.me/nekonymous_bot?start=...

هرکی بازش کنه، می‌تونه برات پیام ناشناس بفرسته.

در همان cleanup، متن‌های ارسال، inbox، تنظیمات و خطاها را هم کوتاه‌تر کردم. هر پیام باید روشن کند چه اتفاقی افتاده و قدم بعدی چیست. زبان گربه‌ای باقی مانده، اما نباید مرز حریم خصوصی یا یک failure واقعی را پشت شوخی پنهان کند.

حتی بعضی وقت‌ها اگر دستور اشتباهی بدهید، وسط کاری گیج شوید یا بات چیزی برای نشان‌دادن نداشته باشد، ممکن است به زبان گربه‌ای با شما حرف بزند:))

برای من این بخش صرفاً شوخی یا تزئین UI نبود.

محصولی که با پیام‌های شخصی و بعضی وقت‌ها حساس آدم‌ها سروکار دارد، نباید شبیه یک پنل امنیتی خشک و ترسناک باشد.

نِکو باید محدودیت‌هایش را جدی بگیرد، ولی خودش بتواند ساده، دوستانه و کمی بازیگوش باقی بماند.

نِکو چه چیزی را تضمین نمی‌کند؟

این بخش شاید از خود معماری هم مهم‌تر باشد.

نِکونیموس یک پیام‌رسان با رمزنگاری سرتاسری نیست.

Telegram پیام را هنگام ارسال و دریافت می‌بیند.

Worker نِکو هم هنگام پردازش، رمزنگاری، رمزگشایی و تحویل، متن پیام را می‌بیند.

اگر کسی کنترل runtime یا secretهای اصلی برنامه را به دست بیاورد، می‌تواند به داده‌هایی که در همان بازه پردازش می‌شوند دسترسی پیدا کند.

گیرنده هم می‌تواند پیام را کپی کند، screenshot بگیرد یا آن را جای دیگری منتشر کند.

نِکو قرار نیست بگوید کاربر از هر زاویه‌ای ناشناس می‌ماند، یا امنیت مطلق و اعتماد صفر فراهم شده است.

چیزی که تلاش می‌کند انجام دهد محدودتر، ولی واقعی‌تر است:

  • کاربران را در جریان معمول محصول از یکدیگر مخفی کند؛
  • متن پیام‌ها را در D1 و KV نگه ندارد؛
  • داده‌های باقی‌مانده را تا جای ممکن غیرقابل اتصال نگه دارد؛
  • payload را بعد از تحویل پاک کند؛
  • retentionها را محدود کند؛
  • block، report، pause و hard reset را در سمت سرور enforce کند؛
  • و درباره قسمت‌هایی که همچنان نیازمند اعتماد به Telegram، Cloudflare و اپراتور هستند صادق باشد.

این داستان نسخه‌ی اول نِکو بود

نسخه‌ی اول نِکو در نهایت از یک سؤال خیلی ساده شروع شد:

آیا می‌شود یک بات پیام ناشناس ساخت که فقط پیام را برساند و برای انجام همین کار، تبدیل به صاحب یک آرشیو از زندگی خصوصی آدم‌ها نشود؟

پاسخی که امروز به آن رسیده‌ام کامل و جادویی نیست.

ولی یک سیستم واقعی، تست‌شده و قابل بررسی است:

  • لینک شخصی پیام ناشناس؛
  • پیام متنی و مدیای پشتیبانی‌شده؛
  • inbox محدود؛
  • پاسخ ناشناس؛
  • نام خصوصی؛
  • block و report؛
  • توقف دریافت پیام؛
  • پاک‌کردن کامل حساب و ساخت هویت تازه؛
  • ارزیابی سبک گفت‌وگو؛
  • hub یکپارچه‌ی پروفایل و پیشنهادها با ذخیره‌ی پیشرفت و وضعیت آمادگی؛
  • پیشنهاد گفت‌وگوی اختیاری؛
  • و معماری تیکت‌های مستقل و مهروموم‌شده.

کل پروژه متن‌باز است.

کد، معماری، Threat Model، محدودیت‌ها و تصمیم‌های فنی پروژه را می‌شود در GitHub دید و بررسی کرد.

من تا جای ممکن کدها و مستندات را بازبینی، تمیز و بهینه کردم تا بات در پایدارترین حالت ممکن، با منابع محدود و مرزهای مشخص کار کند.

هدف نهایی‌اش هم هنوز همان جمله‌ی اول است:

رساندن پیام‌های ناشناس، بدون اینکه برای انجامش بی‌دلیل سرک بکشد توی زندگی و حرف‌های خصوصی آدم‌ها.

نِکو قرار نیست ادعا کند همه‌چیز را حل کرده.

ولی تلاش کرده کاری را که باید انجام دهد، دقیق انجام دهد و چیزهایی را که نباید نگه دارد، نگه ندارد.

هرجا در کد، مستندات، تجربه‌ی کاربری یا همین منطق مشکلی دیدید، حتماً بهم بگید.

میو:)

اشتراک‌گذاری

ادامه مطالعه