مرجع پیادهسازی رمزگذاری
رمزگذاریای که اجرا میشود باریکتر از رمزگذاریای است که نوشته شده، و همین شکاف موضوع این صفحه است. sealed boxهای libsodium تنها رمزگذاری پیامی هستند که Railgun اجرا میکند — و تنها روی مسیر کلاینتی اجرا میشوند که ماژول بومی libsignal در آن غایب است. در بیلد دسکتاپ Electron، جایی که libsignal واقعاً بارگذاری میشود، هندلری که پیامهای مستقیم را رمزگذاری میکند هنوز یک جاینگهدار است که متن ساده را با base64 کدگذاری میکند. پیامهای اجتماع و کانال روی هیچ مسیری رمزگذاری نمیشوند. پروتکل به سبک Signal که در نیمهٔ دوم این صفحه توصیف شده نوشته و بازبینی شده است، و به هیچ کلاینتی که بتوانید دانلود کنید متصل نشده است.
این یک مرجع معماری است، نه ممیزی امنیتی مستقل. بخشهای 1 و 2 آنچه را عرضه میشود توصیف میکنند. بخشهای 4 تا 7 طراحیای را توصیف میکنند که Railgun در حال ساختن آن است و سرتاسر با برچسب برنامهریزیشده مشخص شدهاند. هیچ چیز در اینجا نباید به این معنا خوانده شود که کلاینتی از Railgun هماکنون ویژگیای را که برنامهریزیشده علامت خورده است اعمال میکند.
آنچه امروز عرضه میشود
| مسیر | سازوکار | وضعیت |
|---|---|---|
| پیامهای مستقیم — بیلدهای مرورگر و توسعه | sealed box از libsodium — توافق کلید X25519، رمزگذاری احرازشدهٔ XSalsa20-Poly1305. زمانی انتخاب میشود که ماژول بومی libsignal غایب باشد. | رمزگذاریشده |
| پیامهای مستقیم — بیلد دسکتاپ Electron | libsignal بارگذاری میشود، بنابراین encryptDm به هندلر IPC در Electron هدایت میشود — جاینگهداری که متن ساده را با base64 کدگذاری میکند. رله متن خوانا دریافت میکند. | رمزگذارینشده |
| پیامهای اجتماع و کانال | به صورت متن سادهٔ پیچیدهشده در base64 فرستاده میشوند. رمزگذاری کانال نوشته شده اما یک نشانگر متن ساده برمیگرداند. | رمزگذارینشده |
| X3DH + Double Ratchet | بر پایهٔ libsignal پیادهسازی شده، اما تنها به عنوان یک نوع وارد شده است. هیچ مسیر اجرایی نمونهای از آن نمیسازد. | برنامهریزیشده |
| ذخیرهسازی کلید خصوصی | کلیدها روی دستگاه تولید و نگهداری میشوند. هیچ مسیری در کد، کلید خصوصی یا مادهٔ کلید هویت را منتقل نمیکند. | پیادهسازیشده |
1. sealed box — رمزگذاریای که اجرا میشود
هر جا که Railgun اصلاً یک پیام مستقیم را رمزگذاری کند، این کار را با یک sealed box از libsodium انجام میدهد — ساختار crypto_box_seal. این ساختار توافق کلید X25519 را با رمزگذاری احرازشدهٔ XSalsa20-Poly1305 ترکیب میکند. این رمزنگاری واقعی و بهخوبی بازبینیشده است. همچنین ساختاری عامدانه ساده است و محدودیتهایش اهمیت دارند. اینکه کدام کلاینتها به آن میرسند موضوع بخش 2 است: بیلدهای مرورگر و توسعه میرسند؛ بیلد دسکتاپ Electron نمیرسد.
sealed box چگونه ساخته میشود
// Sender has only the recipient's public key, PK_r
(pk_e, sk_e) = crypto_box_keypair() // fresh, single-use
nonce = BLAKE2b(pk_e || PK_r) // derived, not random
ct = crypto_box(plaintext, nonce, PK_r, sk_e)
sealed = pk_e || ct // ephemeral key is prepended
erase(sk_e)
گیرنده کلید عمومی گذرا را از ابتدای پیام بازیابی میکند، همان راز مشترک را با کلید خصوصی خودش دوباره محاسبه میکند و جعبه را باز میکند. برچسب Poly1305 به این معناست که یک متن رمزشدهٔ دستکاریشده به جای رمزگشایی به دادهٔ بیمعنا، اصلاً باز نمیشود.
این به شما چه میدهد
- • محرمانگی در برابر رله و هر کسی روی مسیر شبکه.
- • یکپارچگی — دستکاری تشخیص داده میشود و بیسروصدا رمزگشایی نمیشود.
- • ناشناسی فرستنده: کلید گذرا هیچ چیزی دربارهٔ فرستنده فاش نمیکند.
- • نیازی به برپاسازی نشست نیست، پس وقتی گیرنده آفلاین است هم کار میکند.
این به شما چه نمیدهد
- • هیچ رازداری پیشرویی وجود ندارد. کلید خصوصی بلندمدت گیرنده هر پیامی را که تا به حال برای او فرستاده شده باز میکند. اگر آن کلید درز کند، متن رمزشدهٔ ضبطشده هم با آن درز میکند.
- • هیچ احراز هویت فرستندهای وجود ندارد. sealed box بنا بر ساختارش ناشناس است؛ هیچ چیزی دربارهٔ نویسندهاش اثبات نمیکند.
- • هیچ بازیابی پس از افشا وجود ندارد. هیچ ratchetی برای عبور از افشای کلید در کار نیست.
این سه شکاف دقیقاً همان چیزی است که X3DH و Double Ratchet برای بستن آنها وجود دارند، و به همین دلیل آن کار در جریان است. تا وقتی که به سرانجام نرسیده، خلاصهٔ صادقانهٔ یک پیام مستقیم مبتنی بر sealed box این است: قوی در برابر ناظر منفعل شبکه و در برابر رله، ضعیف در برابر دشمنی که سرانجام کلید دستگاه گیرنده را به دست بیاورد. این خلاصه فقط جایی صدق میکند که این کد واقعاً اجرا میشود.
2. آنچه رمزگذاری نمیشود
پیامهای اجتماع و کانال — یعنی بخش عمدهٔ آنچه بیشتر کاربران Railgun واقعاً تایپ میکنند — امروز روی هیچ مسیر کلاینتی رمزگذاری نمیشوند. تابع رمزگذاری کانال وجود دارد و دربارهٔ خودش صریح است: یک هشدار ثبت میکند و متن ساده را پیچیده در base64 پشت یک رشتهٔ نشانگر برمیگرداند، تا هیچکس در ادامهٔ مسیر آن را با متن رمزشده اشتباه نگیرد.
// What encryptChannel actually returns
logger.warn("Channel encryption not yet implemented")
ciphertext = base64("[DEVCRYPTO:PLAINTEXT]" + plaintext)
پیامد عملی: برای محتوای اجتماع و کانال، سرور Railgun در همان موقعیتی است که هر سرور چت معمولی دارد. میتواند آنچه را آنجا مینویسید بخواند. توزیع کلید گروهی — یک کلید برای هر کانال که درون sealed box به اعضا داده شود — راهحل برنامهریزیشده است و عرضه نشده است.
پیامهای مستقیم در بیلد دسکتاپ Electron
شکاف دوم پیامهای مستقیم روی کلاینتی است که بیشتر مردم اجرا میکنند، و بهراحتی میتوان آن را وارونه بیان کرد، پس منطق انتخاب را همانگونه که کد اجرایش میکند اینجا میآوریم. initCrypto() از فرایند اصلی میپرسد که آیا libsignal بارگذاری شده است یا نه. @signalapp/libsignal-client یک وابستگی واقعی است و در زمان راهاندازی لازم است، پس در یک بیلد عرضهشدهٔ Electron واقعاً بارگذاری میشود — و همین پاسخ است که پیادهسازی IPC مربوط به Electron را به جای پیادهسازی sealed box انتخاب میکند. encryptDm آن به هندلر crypto:encryptDm ارجاع میدهد، که یک جاینگهدار است:
// crypto:encryptDm, Electron main process
const plaintextBytes = new TextEncoder().encode(plaintext)
ciphertext: toBase64(plaintextBytes) // not encryption
پس sealed boxهای بخش 1 زمانی به کار گرفته میشوند که libsignal غایب باشد — یعنی در مرورگر و در محیط توسعه. روی کلاینت دسکتاپ، یک پیام مستقیم به صورت متن سادهٔ کدگذاریشده با base64 به رله میرسد. libsignal آنجا بارگذاری میشود تا یک کلید هویت و یک بستهٔ پیشکلید تولید کند؛ هیچ نشستی برقرار نمیکند و هیچ ratchetی انجام نمیدهد — crypto:ensureDmSession فقط یک لاگ اشکالزدایی بدون بدنه است.
3. Curve25519 — منحنی زیربنایی
هم sealed boxهایی که عرضه میشوند و هم طراحی X3DH که عرضه نمیشود، بر همان منحنی بیضوی استوارند: Curve25519، طراحیشده به دست Daniel J. Bernstein برای تبادل کلید دیفی-هلمن با سرعت بالا. این بخش خودِ منحنی را توصیف میکند و بر هر دو صدق میکند.
معادلهٔ منحنی
y² = x³ + 486662x² + x
over the prime field 𝔽ₚ where p = 2²⁵⁵ − 19
عدد اول 2²⁵⁵ − 19 انتخاب شد چون حساب پیمانهای بسیار سریعی را ممکن میکند. ضریب 486662 به این دلیل انتخاب شد که کوچکترین مقداری است که منحنی امنی با ویژگیهای امنیتی لازم تولید میکند.
تولید کلید
1. کلید خصوصی: 32 بایت تصادفی با یک CSPRNG (مولد اعداد شبهتصادفی امن از نظر رمزنگاری) تولید کنید. کلید را با صفر کردن 3 بیت پایین و بیت بالا و یک کردن بیت یکیماندهبهآخر مهار کنید. این کار تضمین میکند که کلید مضربی از 8 و در بازهٔ معتبر باشد.
a = random(32 bytes)
a[0] &= 248 // Clear low 3 bits
a[31] &= 127 // Clear high bit
a[31] |= 64 // Set second-highest bit
2. کلید عمومی: ضرب اسکالر کلید خصوصی در نقطهٔ پایهٔ G = 9 را محاسبه کنید.
A = a · G // Scalar multiplication on Curve25519
امنیت بر مسئلهٔ لگاریتم گسسته روی منحنی بیضوی (ECDLP) تکیه دارد: با داشتن A و G، بازیابی a از نظر محاسباتی ناممکن است. Curve25519 تقریباً 128 بیت امنیت فراهم میکند.
تبادل کلید دیفی-هلمن
دو طرف (آلیس و باب) میتوانند یک راز مشترک را بدون آنکه هرگز آن را منتقل کنند محاسبه کنند:
Alice: a (private), A = a·G (public)
Bob: b (private), B = b·G (public)
Alice computes: S = a · B = a · (b·G)
Bob computes: S = b · A = b · (a·G)
Both arrive at S = ab·G — the same shared secret
شنودگری که A و B (هر دو عمومی) را میداند نمیتواند S را بدون حل ECDLP محاسبه کند. این همان فرض دیفی-هلمن محاسباتی (CDH) است.
یک sealed box همین تبادل است که یک سوی آن گذرا شده و دور انداخته میشود. X3DH، در ادامه، همین تبادل است که چهار بار روی جفتکلیدهای متفاوت انجام میشود تا نتیجه، علاوه بر پنهان کردن محتوا، هر دو طرف را نیز احراز کند.
4. X3DH — دیفی-هلمن سهگانهٔ گسترشیافته
برنامهریزیشدهX3DH مسئلهٔ دشواری را حل میکند: چگونه دو نفر وقتی یکی از آنها آفلاین است یک نشست رمزگذاریشده برقرار میکنند؟ دیفی-هلمن سنتی نیازمند آنلاین بودن هر دو طرف است. X3DH از بستههای کلید از پیش بارگذاریشده استفاده میکند تا توافق کلید ناهمگام ممکن شود.
انواع کلید
کلید هویت (IK)
جفتکلید بلندمدت Curve25519. یک بار تولید میشود و کاربر را از نظر رمزنگاری شناسایی میکند. تا وقتی کاربر دوباره ثبتنام نکند هرگز تغییر نمیکند.
پیشکلید امضاشده (SPK)
جفتکلید میانمدت Curve25519 که با کلید هویت امضا شده است. بهطور دورهای چرخانده میشود. امضا ثابت میکند که متعلق به دارندهٔ کلید هویت است.
پیشکلید یکبارمصرف (OPK)
جفتکلیدهای گذرای Curve25519 که دستهدسته بارگذاری میشوند. هر یک یک بار استفاده و سپس حذف میشود. لایهای اضافی از رازداری پیشرو برای پیام نخست فراهم میکند.
کلید گذرا (EK)
جفتکلید تازهٔ Curve25519 که فرستنده برای هر نشست جدید تولید میکند. هرگز روی سرور ذخیره نمیشود.
دستدادن X3DH
وقتی آلیس میخواهد به باب (که ممکن است آفلاین باشد) پیام بدهد، بستهٔ پیشکلید باب را از سرور میگیرد و چهار محاسبهٔ دیفی-هلمن انجام میدهد:
// Alice has: IKₐ (her identity key), EKₐ (fresh ephemeral)
// Bob published: IKᵦ, SPKᵦ, OPKᵦ
DH1 = DH(IKₐ, SPKᵦ) // her identity ↔ his signed pre-key
DH2 = DH(EKₐ, IKᵦ) // her ephemeral ↔ his identity
DH3 = DH(EKₐ, SPKᵦ) // her ephemeral ↔ his signed pre-key
DH4 = DH(EKₐ, OPKᵦ) // her ephemeral ↔ his one-time pre-key
SK = KDF(DH1 || DH2 || DH3 || DH4)
// SK is the shared secret, derived via HKDF-SHA-256
چرا چهار عملیات DH؟
- DH1 احراز هویت دوطرفه را فراهم میکند (هر دو کلید هویت درگیرند)
- DH2 تضمین میکند که کلید گذرا به هویت باب گره خورده است
- DH3 رازداری پیشرو را از طریق کلید گذرا فراهم میکند
- DH4 رازداری پیشروی بیشتری فراهم میکند. اگر هیچ OPK در دسترس نباشد، X3DH تنها با DH1–DH3 هم کار میکند
DH1 همان عملیاتی است که sealed box آن را ندارد. به همین دلیل مسیری که عرضه میشود نمیتواند به شما بگوید یک پیام از چه کسی آمده است.
KDF — تابع استخراج کلید
خروجیهای خام DH با HKDF-SHA-256 (تابع استخراج کلید مبتنی بر HMAC) ترکیب میشوند. HKDF آنتروپی را از خروجیهای بههمچسبیدهٔ DH استخراج میکند و آن را به یک راز مشترک با توزیع یکنواخت تصادفی گسترش میدهد:
PRK = HKDF-Extract(salt="", input=DH1||DH2||DH3||DH4)
SK = HKDF-Expand(PRK, info="RailGunX3DH", length=32)
5. الگوریتم Double Ratchet
برنامهریزیشدهبه محض اینکه X3DH یک راز مشترک اولیه برقرار کند، Double Ratchet کار را به دست میگیرد. این الگوریتم از دو «ratchet» درهمتنیده استفاده میکند تا برای هر پیام کلید یکتای تازهای استخراج کند. طراحی از پیامهای پیشین محافظت میکند و قرار است پس از یک گام ratchet تازه، محافظت از پیامهای آینده را بازیابی کند.
DH Ratchet (نامتقارن)
هر بار که جهت گفتگو عوض میشود (آلیس → باب، سپس باب → آلیس)، یک تبادل کلید دیفی-هلمن تازه با کلیدهای گذرای جدید انجام میشود. این کار کلید ریشه را رو به جلو «ratchet» میکند:
dh_out = DH(my_ratchet_key, their_ratchet_key)
root_key, chain_key = KDF(root_key, dh_out)
این همان چیزی است که امنیت پس از افشا را فراهم میکرد: مهاجمی که وضعیت کنونی را میدزدد، به محض انجام یک گام تازهٔ DH ratchet دسترسیاش را از دست میدهد.
Ratchet متقارن (زنجیرهٔ درهمسازی)
میان گامهای DH ratchet، یک ratchet مبتنی بر درهمسازی برای هر پیام یک کلید پیام تازه از کلید زنجیره استخراج میکند:
message_key = HMAC-SHA256(chain_key, 0x01)
chain_key = HMAC-SHA256(chain_key, 0x02)
کلید زنجیرهٔ قدیمی پس از هر گام حذف میشود. کلید پیام دقیقاً یک پیام را رمزگذاری میکند و سپس حذف میشود. این همان سازوکار رازداری پیشرو است.
پیشروی Ratchet
به تصویر که درآید، Double Ratchet اینگونه است. هر پیکان یک تابع یکطرفه است — میتوانید جلو بروید اما هرگز عقب نه:
Root Key Chain:
RK₀ ──DH──→ RK₁ ──DH──→ RK₂ ──DH──→ RK₃ ...
Each RK step produces a Chain Key:
↓ ↓ ↓
CK₀ CK₁ CK₂
↓ ↓ ↓
Each CK step produces Message Keys:
CK₀→MK₀ CK₁→MK₃ CK₂→MK₅
CK₀→MK₁ CK₁→MK₄ CK₂→MK₆
CK₀→MK₂ CK₂→MK₇
هر MKₙ قرار بود دقیقاً یک پیام را رمزگذاری کند و سپس حذف شود، تا مهاجمی که MK₄ را به دست میآورد نتواند MK₃ یا MK₅ را استخراج کند. روی مسیر sealed box که امروز عرضه میشود، نه کلیدی برای هر پیام هست که دزدیده شود و نه زنجیرهای که پیموده شود — یک کلید بلندمدت هست که همه چیز را باز میکند.
6. رازداری پیشرو — چه چیزی لازم است
برنامهریزیشدهرازداری پیشرو یعنی افشای کلیدهای بلندمدت، کلیدهای نشستهای گذشته را افشا نمیکند. Railgun امروز آن را ندارد. دو سازوکار میتوانستند آن را فراهم کنند و هر دو به پروتکل برنامهریزیشدهٔ بالا تعلق دارند:
کلیدهای گذرا در X3DH
فرستنده برای هر نشست جدید یک جفتکلید گذرای تازه تولید میکند و نیمهٔ خصوصی آن را پس از دستدادن حذف میکند. مهاجمی که بعدها کلیدهای هویت هر دو طرف را بدزدد باز هم نمیتواند راز نشست را بازسازی کند. sealed boxها در سمت فرستنده واقعاً از یک کلید گذرای تازه استفاده میکنند — اما نیمهٔ گیرنده در این تبادل کلید بلندمدت اوست، و دقیقاً به همین دلیل این ویژگی برقرار نیست.
حذف کلیدهای ratchet
Double Ratchet کلیدهای زنجیره و کلیدهای پیام قدیمی را پس از استفاده حذف میکند. زنجیرهٔ KDF یکطرفه است — با داشتن CKₙ میتوانید CKₙ₊₁ را محاسبه کنید اما CKₙ₋₁ را نه، پس افشا در زمان T چیزی از آنچه پیش از T فرستاده شده فاش نمیکند.
امنیت پس از افشا
DH ratchet رازداری آینده را نیز فراهم میکند: مهاجمی که وضعیت کنونی نشست را افشا میکند، آن را در گام بعدی ratchet از دست میدهد، چون آن گام تصادفیبودنی وارد میکند که مهاجم آن را ندارد. بدون ratchet، یک کلید افشاشده تا ابد برای مهاجم مفید باقی میماند.
7. معماری رله و صف
Railgun پاکتها را رله میکند و ممکن است آنها را برای تحویل آفلاین موقتاً در صف بگذارد. رله چنان ساخته شده که به متن سادهٔ پیام نیاز نداشته باشد، اما امروز در دو حالت متن ساده دریافت میکند: پیامهای کانال، که هیچ کلاینتی آنها را رمزگذاری نمیکند، و پیامهای مستقیم از بیلد دسکتاپ Electron، که هندلر IPC آن به جای رمزگذاری، با base64 کدگذاری میکند. یک پیام مستقیم از مسیر sealed box به صورت متن رمزشدهای میرسد که رله نمیتواند بازش کند.
سرور چه میبیند
سرور نگه میدارد:
- • بستههای کلید عمومی
- • اطلاعات ثبتنام کاربر
- • فراداده و عضویت اجتماع/کانال
- • متن رمزشدهٔ sealed box که نمیتواند بازش کند (پیامهای مستقیم از مسیر مرورگر/توسعه)
- • متن سادهٔ پیامهای مستقیم کدگذاریشده با base64 از بیلد دسکتاپ Electron
- • محتوای پیامهای کانال، تا وقتی رمزگذاری کانال عرضه شود
- • سوابق ممیزی احراز هویت
سرور هرگز دریافت نمیکند:
- • کلیدهای خصوصی یا کلیدهای هویت
- • وضعیت نشست کلاینت
- • نسخهٔ ماندگاری از تاریخچهٔ پیامهای شما
پیامها در زمان واقعی از طریق WebSocket به گیرندگان آنلاین رله میشوند. برای دستگاههای آفلاین، پاکتها با حداکثر TTL برابر 30 روز در Redis صف میشوند و به محض اینکه دستگاه دریافتشان را تأیید کند حذف میشوند. هیچ آرشیو پیامی سمت سرور وجود ندارد — تاریخچهٔ کلاینت در حافظهٔ محلی روی دستگاه خودتان میماند، پس دستگاه گمشده یعنی تاریخچهٔ ازدسترفته.
8. خلاصهٔ ویژگیهای امنیتی
| ویژگی | سازوکار | وضعیت |
|---|---|---|
| محرمانگی پیامهای مستقیم | sealed box فقط روی مسیر مرورگر/توسعه؛ هندلر دسکتاپ Electron با base64 کدگذاری میکند | جزئی |
| یکپارچگی پیامهای مستقیم | برچسب Poly1305 روی مسیر sealed box؛ مسیر دسکتاپ Electron یک base64 احرازنشده میفرستد | جزئی |
| ماندن کلیدها روی دستگاه | انبارهٔ کلید محلی؛ هیچ مسیری در کد کلید خصوصی را منتقل نمیکند | پیادهسازیشده |
| تحویل آفلاین | پاکتها با TTL برابر 30 روز در Redis صف میشوند و با دریافت تأیید حذف میشوند | پیادهسازیشده |
| محرمانگی کانال | توزیع کلید گروهی پیادهسازی نشده است؛ محتوا خوانا فرستاده میشود | پیادهسازینشده |
| احراز هویت فرستنده | sealed boxها بنا بر ساختارشان ناشناساند | پیادهسازینشده |
| رازداری پیشرو | نیازمند Double Ratchet است، که هیچ کلاینتی نمونهای از آن نمیسازد | برنامهریزیشده |
| بازیابی پس از افشا | نیازمند DH ratchet است، که هیچ کلاینتی نمونهای از آن نمیسازد | برنامهریزیشده |
| ممیزی مستقل | هیچ ممیزی رمزنگاری تکمیلشدهای از سوی شخص ثالث ادعا نمیشود | تکمیلنشده |
| تضمین پساکوانتومی | هیچ تضمین فراگیری برای امنیت پساکوانتومی ادعا نمیشود | ادعا نشده |
این صفحه از کجا میآید
هر وضعیتی که در بالا آمد از کد منبع کلاینت خوانده شده است، نه از یک سند طراحی. مخزن کلاینت در حال حاضر عمومی نیست، پس نمیتوانیم از شما بخواهیم خودتان آن را بررسی کنید — و همین دلیلی است برای اینکه این صفحه را یک افشاگری بدانید، نه یک اثبات. پرسش دربارهٔ هر ادعای مشخصی در security@railgun.chat پذیرفته میشود.