
enject: إخفاء أسرار .env عن عيون prAIng: الأسرار تعيش في مخازن محلية مشفرة (لكل مشروع) ويتم حقنها مباشرة في التطبيقات وقت التشغيل، دون أن تلمس القرص كنص عادي.
إخفاء أسرار .env عن أعين الذكاء الاصطناعي.
(ملاحظة: كان هذا المشروع يُسمى سابقًا enveil وتمت إعادة تسميته إلى enject)
أدوات الترميز بالذكاء الاصطناعي مثل Claude Code و Copilot و Cursor وغيرها يمكنها قراءة الملفات في دليل مشروعك، مما يعني أن ملف .env النصي العادي هو مجرد تسريب سري ينتظر الحدوث. هذه ليست نظرية. إنها مشكلة معروفة حدثت لي عدة مرات (حتى بعد أن أخبرت Claude صراحةً ألا يتجسس في ملف settings.json الخاص بـ Claude Code). enject يحل هذه المشكلة بضمان أن النصوص السرية لا توجد أبدًا على القرص على الإطلاق. ملف .env الخاص بك يحتوي فقط على مراجع رمزية؛ القيم الحقيقية موجودة في مخزن مشفر محلي ويتم حقنها مباشرة في عمليتك الفرعية عند التشغيل.
هذا المشروع مستوحى من حل/منشور Filip Hric على مدونته، والذي يستخدم مفهومًا مشابهًا بالاعتماد على 1Password. أردت حلاً مكتفيًا ذاتيًا لا يعتمد على خدمات طرف ثالث، مما أدى إلى هذا الحل. ونعم، تم بناء هذا المشروع بالكامل تقريبًا باستخدام Claude Code مع الكثير من التحقق اليدوي والاختبار.
صُمم هذا المشروع بشكل أساسي للتخفيف من المشكلة المعروفة المتمثلة في قراءة أدوات الذكاء الاصطناعي/LLM لأسرار .env عن طريق الخطأ في مشروعك. تشمل الفوائد الإضافية منع تسرب الأسرار إذا تم رفع .env عن طريق الخطأ إلى مستودع، والقدرة على مشاركة ملفات .env التي تحتوي على مراجع بدلاً من النصوص السرية، وإمكانية مشاركة المخزن المشفر نفسه.
لا يعتبر هذا المشروع حلاً سحريًا لمنع وكيل الذكاء الاصطناعي من الحصول على أسرارك. على سبيل المثال، لا يزال بإمكان الوكيل كتابة كود (عن طريق الخطأ أو عبر حقن موجه) يقوم بتسريب الأسرار إلى مخرجات الطرفية أو إلى ملف في وقت التشغيل. ننصح بشدة بعدم الاعتماد على هذه الأداة، أو على ملفات .env بشكل عام، لتخزين أسرار الإنتاج.
ملف .env الخاص بك يبدو هكذا:
DATABASE_URL=en://database_url
STRIPE_KEY=en://stripe_key
PORT=3000
من الناحية الفنية، من الآمن رفعه إلى المستودع (ربما لا تفعل ذلك، رغم ذلك)، والأهم من ذلك: آمن من أي أدوات ذكاء اصطناعي قد تتجسس عليه عن طريق الخطأ (أو ربما ليس عن طريق الخطأ).
عند تشغيل enject run -- npm start، فإنه:
en:// مقابل الخريطة المفكوكةملف المخزن هو كتلة ثنائية. بدون كلمة المرور الرئيسية، لا يمكن تمييزه عن الضوضاء العشوائية. يتم توليد nonce جديد في كل كتابة، لذا فإن إعادة استخدام nonce في AES-GCM مستحيلة. أي تعديل على النص المشفر — حتى لو كان قلب بت واحد — يؤدي إلى فشل المصادقة ورفض فك التشفير.
لا يزال هذا الإصدار في مرحلة ألفا، لذا يتطلب إلحاق أحدث إصدار عند تثبيته بواسطة cargo install
cargo install enject --version 0.2.0-alpha
يتطلب Rust 1.70+.
git clone https://github.com/greatscott/enject
cd enject
cargo build --release
الملف الثنائي المُجمَّع موجود في target/release/enject. قم بتثبيته مرة واحدة في موقع موجود في PATH الخاص بك لتتمكن من تشغيله من أي مشروع:
macOS / Linux (bash أو zsh)
# الخيار أ: ~/.local/bin (لا يتطلب sudo، شائع على Linux)
mkdir -p ~/.local/bin
cp target/release/enject ~/.local/bin/
# الخيار ب: /usr/local/bin (يتطلب sudo، متاح للنظام بأكمله)
sudo cp target/release/enject /usr/local/bin/
# الخيار ج: ~/.cargo/bin (موجود بالفعل في PATH إذا كنت تستخدم rustup)
cp target/release/enject ~/.cargo/bin/
إذا استخدمت الخيار أ ولم تكن ~/.local/bin موجودة بالفعل في PATH الخاص بك، أضف هذا إلى إعدادات shell الخاصة بك (~/.zshrc، ~/.bashrc، أو ~/.bash_profile):
export PATH="$HOME/.local/bin:$PATH"
ثم أعد تحميله:
source ~/.zshrc # أو ~/.bashrc
تحقق من نجاح التثبيت:
enject --version
الملف الثنائي مثبت عالميًا — لا تحتاج إلى إعادة تثبيته أبدًا. لكن كل مشروع يحصل على مخزن مشفر خاص به:
cd your-project
enject init
يقوم هذا بإنشاء .enject/ في الدليل الحالي مع إعدادات المشروع والمخزن المشفر. أضفه إلى .gitignore — لا ينبغي أبدًا رفعه إلى المستودع.
شغّل هذا مرة واحدة لكل مشروع، في جذر المشروع:
enject init
يقوم هذا بتوليد ملح عشوائي بطول 32 بايت، ويكتب .enject/config.toml، وينشئ مخزنًا مشفرًا فارغًا في .enject/store، ويطالبك بتعيين كلمة مرور رئيسية. أضف .enject/ إلى .gitignore — لا ينبغي أبدًا رفع المخزن إلى المستودع.
enject set some_database_url
# يطلب: القيمة لـ 'database_url': (مخفي)
enject set some_api_key
يتم دائمًا إدخال القيم بشكل تفاعلي. لا توجد طريقة لتمرير قيمة كوسيطة سطر أوامر — هذا يمنع ظهور الأسرار في تاريخ الصدفة أو مخرجات ps.
.envDATABASE_URL=en://some_database_url
MY_API_KEY=en://stripe_key
PORT=3000
أسطر KEY=VALUE العادية تمر دون تغيير. يتم حل مراجع en:// فقط.
enject run -- npm start
enject run -- python manage.py runserver
enject run -- cargo run
كل ما بعد -- يتم تمريره حرفيًا إلى نظام التشغيل. ترث العملية الفرعية بيئة shell الكاملة الخاصة بك (لذا PATH و HOME إلخ موجودة) مع إضافة قيم .env فوقها.
enject list # طباعة أسماء المفاتيح المخزنة (بدون القيم أبدًا)
enject delete <key> # إزالة سر
enject import <file> # تشفير جميع القيم في ملف .env نصي، وإعادة كتابته كقالب en://
enject rotate # إعادة تشفير المخزن بكلمة مرور رئيسية جديدة
لا يوجد get ولا export. طباعة قيمة سرية إلى stdout يخلق متجه تسرب يمكن للذكاء الاصطناعي قراءته — الهدف الكامل من enject هو إبقاء القيم بعيدًا عن القرص وأي تيار إخراج قابل للقراءة.
كل خاصية أمنية لها اختبار آلي مطابق ومسار فحص يدوي.
cargo test
31 اختبارًا، جميعها تغطي الادعاءات أدناه.
آليًا: store::password::tests::test_encrypt_decrypt_roundtrip
يحفظ سرًا، ويحافظ على المخزن، ويعيد تحميله من القرص، ويفك تشفيره، ويتحقق من أن القيمة تعود بشكل صحيح. يمر فقط إذا كانت البايتات على القرص عبارة عن نص مشفر صالح — النص العادي سيفشل في فك التشفير.
cargo test store::password::tests::test_encrypt_decrypt_roundtrip
الفحص اليدوي:
enject init # كلمة المرور: test123
enject set mykey # القيمة: my-super-secret
xxd .enject/store | head -5
strings .enject/store
xxd سيظهر بيانات ثنائية. strings لن يُرجع شيئًا — لا توجد تسلسلات ASCII لاستخراجها. أول 12 بايت هي nonce العشوائي؛ كل ما بعدها هو نص مشفر AES-GCM مع علامة مصادقة بطول 16 بايت ملحقة.
آليًا: store::password::tests::test_nonce_changes_on_each_save
يحفظ المخزن مرتين على التوالي، ويقرأ أول 12 بايت من الملف في كل مرة، ويتأكد من اختلافهما.
cargo test store::password::tests::test_nonce_changes_on_each_save
الفحص اليدوي:
xxd .enject/store | head -1 # لاحظ أول 12 بايت
enject set anotherkey # أي كتابة تُدير nonce
xxd .enject/store | head -1 # أول 12 بايت أصبحت مختلفة الآن
آليًا: store::password::tests::test_wrong_password_returns_err
ينشئ مخزنًا بكلمة مرور واحدة، ثم يحاول فتحه بكلمة مرور مختلفة ويتأكد من إرجاع Err.
cargo test store::password::tests::test_wrong_password_returns_err
يدويًا:
enject list # أدخل كلمة المرور الخاطئة
# المخرجات: "Wrong master password or corrupted store."
# رمز الخروج: 1
ينتج AES-GCM علامة مصادقة بطول 16 بايت على النص المشفر. أي تعديل — حتى لو كان قلب بت واحد — يتسبب في فشل التحقق قبل متابعة فك التشفير. لا يتم الكشف عن النص العادي أبدًا.
آليًا: store::password::tests::test_tampered_ciphertext_returns_err
يقلب بايت واحد في منطقة النص المشفر من ملف المخزن (بعد nonce الـ12 بايت)، ثم يحاول فك التشفير ويتأكد من Err.
cargo test store::password::tests::test_tampered_ciphertext_returns_err
يدويًا:
# قلب البايت 20 (داخل النص المشفر، بعد nonce)
python3 -c "
data = open('.enject/store', 'rb').read()
bad = data[:20] + bytes([data[20] ^ 0xFF]) + data[21:]
open('.enject/store', 'wb').write(bad)
"
enject list
# المخرجات: "Wrong master password or corrupted store."
en:// غير محلولإذا كان مرجع في .env ليس له مفتاح مطابق في المخزن، فإن enject run يخرج فورًا برمز غير صفري. لا يتم إطلاق العملية الفرعية أبدًا.
آليًا: env_template::tests::test_unknown_ev_ref_returns_err
يستدعي resolve() بمرجع ليس له إدخال مطابق ويتأكد من Err.
cargo test env_template::tests::test_unknown_ev_ref_returns_err
يدويًا:
echo "DB=en://nonexistent_key" > .env
enject run -- env
# المخرجات: Secret 'nonexistent_key' not found in store. Add it with: enject set nonexistent_key
# رمز الخروج: 1 (لم يتم تشغيل العملية الفرعية `env` أبدًا)
تنفيذ مخزن اختياري/إضافي على مستوى النظام لتسهيل صيانة الأسرار المستخدمة عبر مشاريع متعددة.
تقليل الحاجة إلى إدخال كلمة مرور المخزن يدويًا عند إجراء التحديثات.