
enject: Скрывай .env секреты от prAIng глаз: секреты хранятся в локальных зашифрованных хранилищах (для каждого проекта) и внедряются непосредственно в приложения во время выполнения, никогда не попадая на диск в открытом виде.
Скрывайте секреты .env от глаз AI.
(Примечание: ранее этот проект назывался enveil и был переименован в enject)
Инструменты AI-кодирования, такие как Claude Code, Copilot, Cursor и другие, могут читать файлы в вашем проекте, а это значит, что обычный файл .env в открытом виде — это случайная утечка секретов, ожидающая своего часа. Это не теория. Это известная проблема, которая случалась со мной несколько раз (даже после явного указания Claude не заглядывать в файл settings.json в Claude Code). enject решает эту проблему, гарантируя, что секреты в открытом виде никогда не существуют на диске. Ваш файл .env содержит только символические ссылки; реальные значения хранятся в зашифрованном локальном хранилище и внедряются непосредственно в дочерний процесс при запуске.
Этот проект вдохновлен решением и статьей в блоге Филипа Хрича, где используется аналогичная концепция с 1Password. Мне хотелось получить самодостаточное решение, не зависящее от сторонних сервисов, что и привело к созданию этого решения. Да, этот проект был почти полностью создан с помощью Claude Code с большим объемом ручной проверки и тестирования.
Этот проект в первую очередь предназначен для смягчения известной проблемы случайного чтения секретов .env инструментами AI/LLM в вашем проекте. Дополнительные преимущества включают предотвращение утечки секретов, если .env случайно попадет в репозиторий, возможность делиться файлами .env, содержащими ссылки вместо открытых секретов, а также возможность делиться самим зашифрованным хранилищем.
Этот проект не является серебряной пулей для предотвращения получения ваших секретов агентом AI. Например, агент все еще может написать код (случайно или из-за внедрения подсказок), который выгрузит секреты в вывод терминала или в файл во время выполнения. Мы настоятельно не рекомендуем полагаться на этот инструмент или файлы .env в целом для хранения производственных секретов.
Ваш файл .env выглядит так:
DATABASE_URL=en://database_url
STRIPE_KEY=en://stripe_key
PORT=3000
Технически его можно коммитить (хотя, возможно, этого делать не стоит), и, что более важно, он безопасен для любых инструментов AI, которые случайно (или, возможно, не совсем случайно) в него заглянут.
Когда вы запускаете 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)
# Вариант A: ~/.local/bin (не требует sudo, распространено в Linux)
mkdir -p ~/.local/bin
cp target/release/enject ~/.local/bin/
# Вариант B: /usr/local/bin (требует sudo, доступно системно)
sudo cp target/release/enject /usr/local/bin/
# Вариант C: ~/.cargo/bin (уже в PATH, если вы использовали rustup)
cp target/release/enject ~/.cargo/bin/
Если вы использовали вариант A, и ~/.local/bin еще нет в вашем PATH, добавьте это в ваш конфигурационный файл оболочки (~/.zshrc, ~/.bashrc или ~/.bash_profile):
export PATH="$HOME/.local/bin:$PATH"
Затем перезагрузите его:
source ~/.zshrc # или ~/.bashrc
Проверьте, что все сработало:
enject --version
Бинарник устанавливается глобально — вам не нужно переустанавливать его. Но каждый проект получает свое собственное зашифрованное хранилище:
cd ваш-проект
enject init
Это создает каталог .enject/ в текущей директории с конфигом проекта и зашифрованным хранилищем. Добавьте его в .gitignore — он никогда не должен попадать в коммит.
Запустите один раз для каждого проекта, в корне проекта:
enject init
Это генерирует случайную 32-байтовую соль, записывает .enject/config.toml, создает пустое зашифрованное хранилище в .enject/store и запрашивает установку мастер-пароля. Добавьте .enject/ в ваш .gitignore — хранилище никогда не должно коммититься.
enject set some_database_url
# запрос: Значение для 'some_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
Все, что после --, передается операционной системе без изменений. Дочерний процесс наследует все ваше окружение оболочки (так что PATH, HOME и т.д. присутствуют), а значения из .env накладываются сверху.
enject list # вывести имена сохраненных ключей (никогда значения)
enject delete <key> # удалить секрет
enject import <file> # зашифровать все значения в открытом .env, переписать его как шаблон en://
enject rotate # перешифровать хранилище с новым мастер-паролем
Нет команды get и нет export. Вывод значения секрета в stdout создает вектор утечки, читаемый AI — вся суть 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
Переворачивает один байт в области шифротекста файла хранилища (после 12-байтового nonce), затем пытается расшифровать и проверяет 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` никогда не запускался)
Реализовать опциональное/дополнительное системное хранилище для более удобного обслуживания секретов, используемых в нескольких проектах.
Уменьшить необходимость вручную вводить пароль хранилища при каждом обновлении.