Клиент-серверный менеджер паролей на Go: логины, карты, текст и бинарные файлы. Секреты шифруются на клиенте, на сервере хранятся только ciphertext и метаданные. Управление — через CLI.
Два сервиса — cmd/server (HTTP API, опционально gRPC) и cmd/client (CLI). Написаны по Clean Architecture: presentation → application → domain → adapters.
Общая инфраструктура вынесена в app/internal/pkg, модули не дублируют инфраструктурный код.
| Модуль | Ответственность |
|---|---|
| identity | регистрация, login/logout, session file (JWT access + refresh) |
| vault | CRUD items, envelope-шифрование (MK/DEK), binary upload на сервер |
| sync | локальный SQLite-кэш, offline read-write (pull/push, очередь мутаций, разрешение конфликтов) |
| Модуль | Ответственность |
|---|---|
| identity | пользователи, bcrypt, выдача и ротация JWT (access + refresh) |
| vault | хранение items, multipart upload |
Хранилище: Postgres + MinIO. TLS — в проде на HTTP и (при включённом gRPC) на gRPC-сервере — те же cert_file / key_file.
Внутри одного сервиса модули не импортируют use case соседа напрямую. Схема:
- у потребителя — port в
application; - у поставщика — use cases
application; - связка — intermodule-адаптер в
adapters/client/intermodule, который вызывает use case другого модуля.
Так модули остаются изолированными срезами. При переходе на микросервисы достаточно взять pkg + код модуля и заменить intermodule-адаптер на вызов настоящего HTTP/gRPC API соседнего сервиса.
Сервер: регистрация и аутентификация (bcrypt, JWT access + refresh, logout с отзывом refresh); HTTP REST для identity и vault (всегда); опциональный gRPC для тех же модулей; хранение ciphertext и plaintext metadata в Postgres; крупные binary — в MinIO (single upload и multipart по частям, presigned URL, GC просроченных загрузок); оптимистичная блокировка через revision / If-Match; опциональный TLS в prod.
Клиент: CLI (one-shot и REPL session); транспорт к API сервера — protocol: http (по умолчанию) или protocol: grpc; типы items — credential, card, text, binary; envelope на клиенте (MK + DEK, Argon2id, AES-GCM) — сервер не видит plaintext секретов; ротация DEK (rotate-dek) и смена master password с пересохранением wrapped_dek (rewrap-master-password); локальный SQLite-кэш; sync pull с сервера; offline read-write: list/get из кэша, create/update/delete в очередь → sync push; разрешение конфликтов (sync resolve: local / server / lww); resume прерванного multipart upload; session file с авто-refresh access token на vault-командах.
Сквозной сценарий: один пользователь — несколько устройств; данные синхронизируются через vault API + клиентский sync.
| Компонент | Назначение |
|---|---|
| Go 1.24+ | сборка server/client |
| Docker | PostgreSQL и MinIO для сервера; contract/component/integration/e2e-тесты |
| SQLite | локальный кэш клиента |
Приоритет настроек: флаги CLI → переменные окружения GOPHKEEPER_* → yaml → defaults. Примеры env — app/.env.example.
Пошаговый сценарий: поднять инфраструктуру → миграции → сервер → клиент → пройти все команды.
Из корня репозитория:
make build
# бинарники: app/bin/server, app/bin/client
# version/date вшиваются через -ldflags (см. Makefile)Без make:
go build -tags=go_json -o app/bin/server ./app/cmd/server
go build -tags=go_json -o app/bin/client ./app/cmd/clientПроверка:
./app/bin/client --version
./app/bin/server --versiondocker run -d --name gophkeeper-postgres \
-e POSTGRES_USER=gophkeeper \
-e POSTGRES_PASSWORD=gophkeeper \
-e POSTGRES_DB=gophkeeper \
-p 5432:5432 \
postgres:16-alpineДождаться готовности или:
docker logs -f gophkeeper-postgres
# database system is ready to accept connectionsОстановка / удаление:
docker stop gophkeeper-postgres
docker rm gophkeeper-postgresMinIO хранит крупные binary-файлы vault. Бакет gophkeeper создаётся сервером при старте.
docker run -d --name gophkeeper-minio \
-p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
minio/minio server /data --console-address ":9001"Веб-консоль MinIO: http://127.0.0.1:9001 (логин/пароль minioadmin).
Остановка / удаление:
docker stop gophkeeper-minio
docker rm gophkeeper-minioexport DATABASE_DSN='postgres://gophkeeper:gophkeeper@localhost:5432/gophkeeper?sslmode=disable'
make migrateили:
go run ./app/cmd/migrate -d "$DATABASE_DSN"Миграции лежат в migrations/gophkeeper/. Клиентский SQLite мигрируется автоматически при первом запуске client (файл gophkeeper.db в cache dir).
Минимум для dev (можно положить в app/.env):
export GOPHKEEPER_AUTH_JWT_SECRET=change-me-in-dev
export GOPHKEEPER_DATABASE_URI='postgres://gophkeeper:gophkeeper@localhost:5432/gophkeeper?sslmode=disable'
export GOPHKEEPER_MINIO_ENDPOINT=localhost:9000
export GOPHKEEPER_MINIO_PUBLIC_ENDPOINT=localhost:9000
export GOPHKEEPER_MINIO_ACCESS_KEY=minioadmin
export GOPHKEEPER_MINIO_SECRET_KEY=minioadmin
export GOPHKEEPER_MINIO_USE_SSL=false
export GOPHKEEPER_MINIO_BUCKET=gophkeeperТерминал 1:
make run-server
# или: ./app/bin/server -c app/configs/server.yamlСервер слушает http://127.0.0.1:8080 (см. app/configs/server.yaml). gRPC по умолчанию выключен (grpc.address: "").
Терминал 2 (нужен интерактивный терминал — для master password и опционально login password):
make run-client
# эквивалент: ./app/bin/client -c app/configs/client.yamlРегистрация и вход (пароль login — флагом; master password — отдельно, для vault):
./app/bin/client register --login alice --password 'login-secret'
./app/bin/client login --login alice --password 'login-secret'Список items (без master password):
./app/bin/client items list
./app/bin/client sync pullСоздание text-item (запросит Master password в терминале):
./app/bin/client items create \
--type text \
--metadata '{"title":"note"}' \
--data 'hello world'Чтение (снова master password):
./app/bin/client items get --id 1Выход из учётки:
./app/bin/client logoutВажно: команды vault (
items create/get/update,rotate-dek,rewrap-master-password) запрашивают master password через скрытый ввод (term.ReadPassword). В pipe/скриптах без TTY на Windows будет ошибкаThe handle is invalid— выполняйте их в обычном терминале. Login password дляregister/loginможно передать флагом--password.
Каталог кэша по умолчанию: ~/.cache/gophkeeper (переопределяется --cache-dir / GOPHKEEPER_CACHE_DIR).
| Файл | Содержимое |
|---|---|
gophkeeper.db |
SQLite: снимок зашифрованных items, sync state, offline-очередь, сессии binary upload |
~/.config/gophkeeper/session.json |
JWT access + refresh (файл сессии, флаг --session-file) |
Отдельный контейнер SQLite не требуется — это встроенная БД в файле на диске клиента.
Очистка только локального кэша (сессия не трогается):
./app/bin/client cache clearУказываются до подкоманды:
gophkeeper [глобальные флаги] <команда> [флаги команды]
| Флаг | Короткий | Env | Описание |
|---|---|---|---|
--config |
-c |
CONFIG |
путь к yaml (app/configs/client.yaml) |
--server |
-a |
GOPHKEEPER_IDENTITY_HTTP_SERVER_ADDRESS |
URL сервера при protocol=http (vault — тот же) |
--protocol |
GOPHKEEPER_PROTOCOL |
http (default) или grpc |
|
--grpc-address |
GOPHKEEPER_GRPC_ADDRESS |
адрес gRPC (host:port), обязателен при protocol=grpc |
|
--log-level |
-l |
GOPHKEEPER_LOGGER_LEVEL |
уровень логов |
--cache-dir |
GOPHKEEPER_CACHE_DIR |
каталог локального кэша / SQLite | |
--session-file |
GOPHKEEPER_IDENTITY_SESSION_PATH |
файл JWT-сессии | |
--tls-ca-file |
GOPHKEEPER_TLS_CA_FILE |
PEM CA для HTTPS (prod / self-signed) | |
--help |
-h |
дерево всех команд (cobra) |
Примеры:
./app/bin/client -c app/configs/client.yaml --help
./app/bin/client -a http://127.0.0.1:8080 login --login alice --password secret
./app/bin/client --cache-dir /tmp/gk-test items list| Команда | Флаги | Описание |
|---|---|---|
register |
--login, --password |
регистрация + сохранение сессии |
login |
--login, --password |
вход + сохранение сессии |
logout |
— | отзыв refresh на сервере + очистка session file |
./app/bin/client register --login bob --password 'bob-pass'
./app/bin/client login --login bob --password 'bob-pass'
./app/bin/client logoutЕсли --password не указан, пароль запрашивается интерактивно.
Типы: credential, card, text, binary.
| Команда | Флаги | Master password | Описание |
|---|---|---|---|
items create |
--type, --metadata (JSON), --data (- = stdin) |
да | создать item |
items update |
--id, --type, --metadata, --data |
да | обновить |
items list |
— | нет | список (metadata, JSON) |
items get |
--id, --save-to (только binary) |
да | расшифровать и вывести / сохранить файл |
items delete |
--id |
нет | удалить |
items rotate-dek |
--id |
да | новый DEK, тот же MK |
items rewrap-master-password |
— | да (старый + новый ×2) | пересохранить wrapped_dek для всех items |
items resume-create |
— | нет | продолжить прерванный multipart create |
items resume-update |
--id |
нет | продолжить multipart update |
items cancel-upload |
— | нет | отменить незавершённый multipart upload |
Примеры:
# text
./app/bin/client items create --type text \
--metadata '{"title":"pin"}' --data '1234'
# credential (данные — произвольный plaintext, формат на усмотрение клиента)
./app/bin/client items create --type credential \
--metadata '{"site":"example.com"}' \
--data '{"login":"user","password":"pass"}'
# card
./app/bin/client items create --type card \
--metadata '{"label":"visa"}' \
--data '{"number":"4111","exp":"12/30","cvv":"123","holder":"A B"}'
# binary (обязательно --save-to при get; --data - читает stdin)
./app/bin/client items create --type binary \
--metadata '{"filename":"doc.pdf"}' \
--data - < /path/to/file.pdf
./app/bin/client items get --id 4 --save-to /tmp/out.bin
./app/bin/client items delete --id 2
./app/bin/client items rotate-dek --id 1
./app/bin/client items rewrap-master-password # старый MK → новый MK (дважды подтвердить)При offline create/update/delete ставятся в очередь; после восстановления сети — sync push.
Порог задаётся в app/configs/server.yaml:
minio:
multipart_part_size: 5242880 # 5 MiB| Размер ciphertext | Режим | Куда грузится |
|---|---|---|
| ≤ 5 MiB | single (presigned PUT) | MinIO одним запросом |
| > 5 MiB | multipart | MinIO частями по multipart_part_size |
Клиент шифрует файл целиком (envelope), затем режет ciphertext на части. Метаданные item остаются в Postgres; тело binary — в MinIO.
Пример 9 MiB (2 части: 5 + 4 MiB) — в интерактивном терминале:
dd if=/dev/urandom of=/tmp/big.bin bs=1M count=9
./app/bin/client items create --type binary \
--metadata '{"filename":"big.bin","size":9437184}' \
--data - < /tmp/big.bin
# Master password: ...
ID=<напечатанный id>
./app/bin/client items get --id "$ID" --save-to /tmp/out.bin
cmp /tmp/big.bin /tmp/out.binОбновление крупного binary (items update) использует тот же multipart-порог.
Команды resume — если загрузка оборвалась после InitBinaryUpload, но до Complete:
| Команда | Когда |
|---|---|
items resume-create |
прерванный multipart create (сессия в SQLite клиента) |
items resume-update --id N |
прерванный multipart update |
items cancel-upload |
отменить незавершённую сессию |
Обычный items create в одном запуске загружает все части подряд; resume нужен после сбоя сети/процесса между частями. Проверка: items resume-create без сессии → no persisted binary upload session.
| Команда | Флаги | Описание |
|---|---|---|
sync pull |
— | скачать все items с сервера в локальный кэш |
sync push |
— | отправить offline-очередь на сервер |
sync status |
— | показать pending mutations |
sync resolve |
--mutation-id, --strategy |
разрешить конфликт (local, server, lww) |
./app/bin/client sync pull
./app/bin/client sync status
./app/bin/client sync push
./app/bin/client sync resolve --mutation-id <uuid> --strategy serverПри недоступном сервере — offline read-write: items list / items get из кэша; create/update/delete — в очередь, затем sync push.
Типичный offline-поток:
# сервер недоступен — create/delete попадают в очередь
./app/bin/client items create --type text --metadata '{}' --data 'offline'
./app/bin/client sync status # видны pending mutations
# сервер снова доступен:
./app/bin/client sync push
# при конфликте:
./app/bin/client sync resolve --mutation-id <uuid> --strategy local|server|lww
./app/bin/client sync push| Команда | Описание |
|---|---|
cache clear |
удалить локальный снимок items (SQLite cache) |
./app/bin/client -c app/configs/client.yaml session
# gophkeeper> login --login alice --password secret
# gophkeeper> items list
# gophkeeper> exitГлобальные флаги (-c, -a, --protocol, …) задаются при запуске session. Строки в REPL — только команды cobra без повторного partitionArgs. Конфиг и remote gateways (HTTP или gRPC) фиксируются на старте процесса.
По умолчанию везде HTTP. gRPC включается явно на сервере и клиенте.
Сервер — непустой grpc.address в yaml или флаг --grpc-address:
# app/configs/server.yaml
grpc:
address: "127.0.0.1:9090"HTTP REST при этом остаётся на server.address (8080). В prod те же cert_file / key_file, что и для HTTPS, применяются и к gRPC.
Клиент — protocol: grpc и адрес:
# app/configs/client.yaml
protocol: grpc
grpc:
address: "127.0.0.1:9090"или флаги:
./app/bin/client --protocol grpc --grpc-address 127.0.0.1:9090 login --login alice --password secretmake certs
# certs/ca.pem, certs/server.crt, certs/server.keyexport GOPHKEEPER_AUTH_JWT_SECRET=change-me-in-production
export GOPHKEEPER_DATABASE_URI='postgres://gophkeeper:gophkeeper@localhost:5432/gophkeeper?sslmode=disable'
export GOPHKEEPER_MINIO_ENDPOINT=localhost:9000
export GOPHKEEPER_MINIO_PUBLIC_ENDPOINT=localhost:9000
export GOPHKEEPER_MINIO_ACCESS_KEY=minioadmin
export GOPHKEEPER_MINIO_SECRET_KEY=minioadmin
export GOPHKEEPER_MINIO_USE_SSL=false # true, если MinIO тоже на HTTPS
export GOPHKEEPER_MINIO_BUCKET=gophkeeperЕсли MinIO в Docker на HTTP, а в server.prod.yaml стоит minio.use_ssl: true, переопределите GOPHKEEPER_MINIO_USE_SSL=false.
make run-server-prod # https://0.0.0.0:8443
make run-client-prod # client.prod.yaml + tls.ca_file: certs/ca.pemКлиент: https://127.0.0.1:8443. Для публичного CA (Let's Encrypt) tls.ca_file можно не задавать.
| Dev | Prod | |
|---|---|---|
| Конфиги | server.yaml / client.yaml |
server.prod.yaml / client.prod.yaml |
| URL | http://127.0.0.1:8080 |
https://127.0.0.1:8443 |
| TLS на сервере | нет | server.crt + server.key |
| CA на клиенте | не нужен | certs/ca.pem для self-signed |
| gRPC | выключен (grpc.address: "") |
опционально; при protocol: grpc на клиенте — tls.ca_file обязателен в client.prod.yaml |
TLS защищает транспорт; шифрование vault (MK/DEK) — отдельный слой поверх HTTPS/gRPC.
| Флаг | Env | Описание |
|---|---|---|
-c / --config |
CONFIG |
yaml конфиг |
--address |
GOPHKEEPER_SERVER_ADDRESS |
listen address |
--database-uri |
GOPHKEEPER_DATABASE_URI |
Postgres DSN (обязателен) |
--jwt-secret |
GOPHKEEPER_AUTH_JWT_SECRET |
секрет JWT (обязателен) |
--log-level |
GOPHKEEPER_LOGGER_LEVEL |
уровень логов |
--tls-cert |
GOPHKEEPER_SERVER_CERT_FILE |
TLS cert (prod) |
--tls-key |
GOPHKEEPER_SERVER_KEY_FILE |
TLS key (prod) |
--grpc-address |
GOPHKEEPER_GRPC_ADDRESS |
gRPC listen (host:port); пусто — gRPC выключен |
MinIO — через env / yaml (GOPHKEEPER_MINIO_*), см. app/configs/server.yaml:
| Поле yaml | Env | Описание |
|---|---|---|
minio.endpoint |
GOPHKEEPER_MINIO_ENDPOINT |
адрес MinIO для сервера |
minio.public_endpoint |
GOPHKEEPER_MINIO_PUBLIC_ENDPOINT |
адрес в presigned URL для клиента |
minio.access_key |
GOPHKEEPER_MINIO_ACCESS_KEY |
ключ |
minio.secret_key |
GOPHKEEPER_MINIO_SECRET_KEY |
секрет |
minio.bucket |
GOPHKEEPER_MINIO_BUCKET |
имя бакета (по умолчанию gophkeeper) |
minio.use_ssl |
GOPHKEEPER_MINIO_USE_SSL |
HTTPS к MinIO |
minio.multipart_part_size |
GOPHKEEPER_MINIO_MULTIPART_PART_SIZE |
размер части multipart (байты) |
Postgres и MinIO обязательны для старта сервера.
Пирамида: unit → contract → integration → component → e2e. Для contract, component, integration и e2e нужен Docker (testcontainers: Postgres + MinIO).
make test # unit
make test-contract # контракт client - server
make test-integration # репозитории Postgres / MinIO / SQLite
make test-component # HTTP-сценарии сервера: identity, vault
make test-e2e # полные сценарии: auth, items, sync, offline
make test-all # все цели вышеПосле изменения HTTP dto (struct, json-теги):
make generate-easyjsonПосле изменения converter-интерфейсов в adapters/.../converter/ (entity - model, intermodule):
make generate-goverterПосле изменения port-интерфейсов:
make generate-mocksПосле изменения .proto (identity, vault):
make generate-protomake build # app/bin/server + client
make build-all # кросс-сборка linux/windows/darwin → app/bin/dist/
make certs # dev TLS
make run-server # HTTP dev
make run-client # HTTP dev client
make run-server-prod # HTTPS prod
make run-client-prod # HTTPS prod client
make migrate # Postgres миграции (нужен DATABASE_DSN)
make test # unit-тесты (см. раздел «Тесты»)
make test-contract # контракт client ↔ server
make test-component # component-тесты сервера
make test-integration # postgres/minio/sqlite integration
make test-e2e # полные сценарии (testcontainers)
make test-all # всё вместе
make cover-unit # покрытие unit
make cover # покрытие unit + integration
make generate-easyjson # easyjson для HTTP dto
make generate-goverter # goverter для adapters/.../converter
make generate-mocks # gomock для application/port
make generate-proto # protobuf / gRPC stubs