
حافظ بسيط وآمن للأسرار
Keeper هو مخزن تشفير سري للغة Go. يقوم بتشفير أي حمولات بايت تعسفية عند التخزين باستخدام اشتقاق المفتاح Argon2id والتشفير الموثّق XChaCha20-Poly1305 (الافتراضي)، ويخزّنها في قاعدة بيانات bbolt مضمنة.
يأتي على ثلاثة أشكال يمكن استخدامها بشكل مستقل:
x/keephandler) — قم بتركيب نقاط نهاية Keeper على أي موزع net/http في استدعاء واحد، مع خطافات قابلة للتوصيل، وحراس، ومشفِّرات استجابة للتحكم في الوصول وتسجيل التدقيق.cmd/keeper) — واجهة طرفية مع جلسة REPL دائمة، إدخال سري بدون صدى، وعدم تعريض سجل الصدفة.تم تصميم Keeper كطبقة إدارة أسرار أساسية لموازن التحميل Agbero لكنه لا يعتمد على Agbero ويعمل في أي مشروع Go.
يقوم Keeper بتقسيم الأسرار إلى مجموعات. لكل مجموعة سياسة أمان مجموعة (BucketSecurityPolicy) غير قابلة للتغيير تحكم كيفية حماية مفتاح تشفير البيانات (DEK) الخاص بها. تتوفر أربعة مستويات.
المخطط هو بادئة URI تجمع المجموعات ذات الصلة (vault://، certs://، space://، أو أي اسم تسجّله). مستوى الأمان هو خاصية لسياسة المجموعة تُعيّن عند الإنشاء وتظل غير قابلة للتغيير بعد ذلك.
يمكنك مزج مستويات الأمان بحرية داخل نفس المخطط. على سبيل المثال، vault://system قد يكون LevelPasswordOnly (يفتح تلقائيًا عند بدء التشغيل)، بينما vault://admin هو LevelAdminWrapped (يتطلب بيانات اعتماد صريحة).
يتم اشتقاق DEK للمجموعة من المفتاح الرئيسي باستخدام HKDF-SHA256 مع سلسلة معلومات مفصولة بالمجال لكل مجموعة (keeper-bucket-dek-v1:scheme:namespace). جميع مجموعات LevelPasswordOnly تُفتح تلقائيًا عند استدعاء UnlockDatabase بكلمة المرور الرئيسية الصحيحة. لا حاجة لبيانات اعتماد لكل مجموعة في وقت التشغيل. هذا المستوى مناسب للأسرار التي تحتاجها العملية عند بدء التشغيل دون تدخل بشري.
للمجموعة DEK مولد عشوائيًا بحجم 32 بايت وفريد لتلك المجموعة. لا يُخزَّن DEK أبدًا بنص عادي. لكل مسؤول مصرح له، يتم اشتقاق مفتاح تشفير المفاتيح (KEK) من HKDF(masterKey‖adminCred, dekSalt) ويستخدم لتغليف DEK عبر XChaCha20-Poly1305. لا يمكن الوصول إلى المجموعة حتى يقوم مسؤول باستدعاء UnlockBucket ببيانات اعتماده. لا يمكن لكلمة المرور الرئيسية وحدها فك تشفير المجموعة. إلغاء صلاحية مسؤول واحد لا يؤثر على النسخة المغلفة لأي مسؤول آخر.
يتم إنشاء DEK للمجموعة في وقت CreateBucket ويُغلّف فورًا بواسطة HSMProvider يوفره المتصل. يقوم المزوّد بعمليات التغليف وفك التغليف — لا يتعامل Keeper مع DEK الخام بعد تسليمه للمزوّد. يستدعي UnlockDatabase تلقائيًا المزوّد لفك التغليف وتغذية الغلاف (Envelope) لجميع مجموعات HSM المسجلة. تدوير المفتاح الرئيسي لا يُعيد تشفير هذه المجموعات؛ DEK يتحكم فيه المزوّد.
تتوفر في pkg/hsm تطبيق SoftHSM مدمج يعتمد على مفتاح تغليف محمي بـ memguard للاختبار وبيئات CI. لا تستخدمه في الإنتاج.
مطابق لـ LevelHSM في سلوك إدارة المفاتيح، لكن HSMProvider يتم تنفيذه بواسطة pkg/remote.Provider — وهو محول HTTPS قابل للتكوين يفوض التغليف وفك التغليف إلى أي خدمة KMS عن بُعد عبر TLS. توجد تكوينات مسبقة لـ HashiCorp Vault Transit و AWS KMS و GCP Cloud KMS في pkg/remote. للاستخدام في الإنتاج، قم بتكوين TLSClientCert و TLSClientKey لتفعيل مصادقة TLS المتبادلة.
salt ← random 32 bytes, generated once, stored as a versioned SaltStore (unencrypted) masterKey ← Argon2id(passphrase, salt, t=3, m=64 MiB, p=4) → 32 bytes
يتم تخزين تجزئة التحقق عند الاشتقاق الأول:```
verifyHash ← Argon2id(masterKey, "verification", t=1, m=64 MiB, p=4) → 32 bytes
بعد ذلك، تقوم استدعاءات DeriveMaster بإعادة حساب هذا التجزئة ومقارنته مع
crypto/subtle.ConstantTimeCompare. يؤدي عدم التطابق إلى إرجاع ErrInvalidPassphrase.
يتم تخزين ملح KDF غير مشفر حسب التصميم. يجب أن يكون قابلاً للقراءة قبل
UnlockDatabase لاشتقاق المفتاح الرئيسي — تشفيره بمفتاح مشتق من المفتاح الرئيسي سيكون دائريًا. ملح KDF ليس سرًا؛ الغرض منه هو التفرد، وليس السرية.
يتم تشفير كل قيمة نصية واضحة باستخدام XChaCha20-Poly1305 باستخدام مفتاح تشفير البيانات للمجموعة (bucket DEK):``` nonce ← random 24 bytes ciphertext ← XChaCha20-Poly1305.Seal(nonce, DEK, plaintext)
السجل المخزّن هو بنية `Secret` مشفّرة بـ msgpack تحتوي على النص المشفر، والبيانات الوصفية المشفّرة، وإصدار المخطط. المصادقة ضمنية: أي نص مشفر يتم فك تشفيره بمفتاح خاطئ ينتج عنه فشل في مصادقة AEAD قبل إرجاع أي نص واضح.
### KEK derivation — LevelAdminWrapped```
salt ← random 32 bytes, generated at bucket creation, stored in policy
ikm ← masterKey ‖ adminCredential
KEK ← HKDF-SHA256(ikm, salt, info="keeper-kek-v1") → 32 bytes
wrappedDEK ← XChaCha20-Poly1305.Seal(nonce, KEK, DEK)
يتم اشتقاق KEK باستخدام HKDF بدلاً من تمريرة Argon2 ثانية. المفتاح الرئيسي تم إنتاجه بالفعل بواسطة KDF عالي التكلفة؛ استدعاء ثانٍ لـ Argon2 من شأنه أن يضيف مئات المللي ثانية من زمن الانتظار لكل استدعاء UnlockBucket دون أي فائدة أمنية. تعمل HKDF-SHA256 في حوالي ميكروثانية واحدة.
دفاع متعمق: المهاجم الذي يخترق قاعدة البيانات فقط يحصل على DEK المغلف وملح HKDF لكنه لا يستطيع اشتقاق KEK بدون المفتاح الرئيسي. المهاجم الذي يخترق المفتاح الرئيسي فقط لا يستطيع فك تغليف أي DEK من نوع LevelAdminWrapped دون معرفة بيانات اعتماد المشرف أيضًا.
يتم تشفير البيانات الوصفية السرية (وقت الإنشاء، وقت التحديث، عدد مرات الوصول، الإصدار) بشكل منفصل عن النص المشفر:``` metaKey ← HKDF-SHA256(bucketDEK, nil, info="keeper-metadata-v1") → 32 bytes encryptedMeta ← XChaCha20-Poly1305.Seal(nonce, metaKey, msgpack(metadata))
بالنسبة للحاويات `LevelAdminWrapped` و `LevelHSM` و `LevelRemote`، هذا يعني أن البيانات الوصفية غير قابلة للوصول بدون بيانات اعتماد الحاوية، مما يمنع المهاجم الذي لديه حق الوصول للقراءة إلى ملف قاعدة البيانات من معرفة أنماط الوصول أو الطوابع الزمنية.
**ملاحظة حول القنوات الجانبية الزمنية:** يقوم XChaCha20-Poly1305 بمعالجة النص المشفر بالكامل قبل إرجاع خطأ في المصادقة. مسار فك التشفير الاحتياطي (مفتاح تشفير البيانات المستمد الجديد → مفتاح رئيسي قديم كمفتاح تشفير بيانات) يستغرق نفس الوقت الفعلي بغض النظر عن أي مفتاح ينجح. لا يوجد قناة جانبية زمنية تسرب حالة ترحيل السجل.
### تشفير البيانات الوصفية — السياسات، سجل الكتابة المسبقة (WAL)، والتدقيق
جميع البيانات الوصفية الهيكلية مشفرة أيضًا أثناء التخزين. يتم اشتقاق مفتاحين من المفتاح الرئيسي عند وقت `UnlockDatabase`:```
policyEncKey ← HKDF-SHA256(masterKey, nil, info="keeper-policy-enc-v1") → 32 bytes
auditEncKey ← HKDF-SHA256(masterKey, nil, info="keeper-audit-enc-v1") → 32 bytes
policyEncKey يُشفر: قيم BucketSecurityPolicy وWAL التدوير.
auditEncKey يُشفر: حقول Scheme وNamespace وDetails لكل حدث تدقيق.
يتم مسح كلا المفتاحين من الذاكرة عند Lock(). التشفير المستخدم لتشفير البيانات الوصفية هو نفس واجهة crypt.Cipher القابلة للتكوين المستخدمة للأسرار — اختيار المستخدم للتشفير (AES-256-GCM لـ FIPS، XChaCha20-Poly1305 افتراضيًا) يتم تطبيقه تلقائيًا.
تنسيق الإرسال لجميع كتل البيانات الوصفية المشفرة:``` nonce (cipher.NonceSize() bytes) || AEAD-ciphertext
### تجزئة مفتاح دلو السياسة
مفاتيح السياسة على القرص هي تجزئات غير شفافة بدلاً من سلاسل `scheme:namespace` بنص عادي، مما يمنع تعداد أسماء الدلائل دون اتصال:```
base ← hex(SHA-256("scheme:namespace"))[:32] // 32 hex chars = 128-bit key space
_policies/<base> → encrypted BucketSecurityPolicy
_policies/<base>__hash__ → SHA-256(encrypted policy bytes)
_policies/<base>__hmac__ → HMAC-SHA256(policyKey, encrypted policy bytes)
لا يزال schemeRegistry في الذاكرة يستخدم "scheme:namespace" كمفتاح له — فقط تمثيل التخزين على القرص هو الذي يتغير.
يحمل كل سجل سياسة علامتي تكامل تُكتبان بشكل ذري في معاملة bbolt واحدة:``` hash ← SHA-256(encryptedPolicyBytes) — unauthenticated, pre-unlock integrity policyKey ← HKDF-SHA256(masterKey, nil, info="keeper-policy-hmac-v1") → 32 bytes hmac ← HMAC-SHA256(policyKey, encryptedPolicyBytes) — authenticated, post-unlock integrity
قبل `UnlockDatabase`، يكون هاش SHA-256 فقط متاحًا. بعد فتح القفل، يتحقق `loadPolicy` من علامة HMAC. يستدعي `UnlockDatabase` دالة `upgradePolicyHMACs` لملء علامات HMAC للسياسات التي تم إنشاؤها قبل وجود هذه الميزة.
### تدقيق HMAC signing```
auditKey ← HKDF-SHA256(masterKey, nil, info="keeper-audit-hmac-v1") → 32 bytes
HMAC ← HMAC-SHA256(auditKey, event fields including Seq)
يتم تنشيط مفتاح التوقيع في UnlockDatabase ويتم مسحه في Lock. وعندما يتم تدوير المفتاح الرئيسي، يقوم Rotate بإلحاق حدث تدقيق بنقطة تفتيش تدوير المفتاح بكل سلسلة تدقيق نشطة، موقعة بالمفتاح القديم للتدقيق كحدث أخير للحقبة القديمة. لا تتم إعادة كتابة التاريخ أبدًا؛ نقطة التفتيش هي جسر الثقة بين الحقب.
passphrase │ └─ Argon2id(salt) ──→ masterKey (32 bytes, memguard Enclave) │ ├─ HKDF("keeper-audit-hmac-v1") ──→ auditKey (HMAC signing) ├─ HKDF("keeper-audit-enc-v1") ──→ auditEncKey (audit field encryption) ├─ HKDF("keeper-policy-hmac-v1") ──→ policyKey (policy HMAC) ├─ HKDF("keeper-policy-enc-v1") ──→ policyEncKey (policy/WAL encryption) │ ├─ [LevelPasswordOnly] │ └─ HKDF("keeper-bucket-dek-v1:scheme:ns") ──→ DEK │ └─ HKDF("keeper-metadata-v1") ──→ metaKey │ ├─ [LevelAdminWrapped] │ ├─ random 32 bytes ──→ DEK │ │ └─ HKDF("keeper-metadata-v1") ──→ metaKey │ │ │ └─ HKDF("keeper-kek-v1", masterKey‖adminCred, dekSalt) │ └─ KEK │ └─ XChaCha20-Poly1305(KEK, DEK) ──→ wrappedDEK │ └─ [LevelHSM / LevelRemote] ├─ random 32 bytes ──→ DEK │ └─ HKDF("keeper-metadata-v1") ──→ metaKey │ └─ HSMProvider.WrapDEK(DEK) ──→ wrappedDEK (stored; provider controls the wrapping key)
تُصفّر جميع المفاتيح الوسيطة فور استخدامها. ولا يُكتب المفتاح الرئيسي على القرص بأي شكل.
---
## مخطط التخزين
قاعدة البيانات الأساسية هي bbolt. جميع الحاويات (buckets) ومحتوياتها:
| حاوية bbolt | المفتاح (Key) | القيمة (Value) |
|---|---|---|
| `__meta__` | `salt` | msgpack — SaltStore (غير مشفرة؛ اعتماد دائري إذا كانت مشفرة) |
| `__meta__` | `verify` | بايتات خام — تجزئة التحقق Argon2id |
| `__meta__` | `rotation_wal` | `nonce‖AEAD(msgpack(RotationWAL))` |
| `__meta__` | `bucket_dek_done` | `"1"` — علامة إتمام ترحيل DEK |
| `__policies__` | `hex(SHA-256(scheme:ns))[:32]` | `nonce‖AEAD(msgpack(BucketSecurityPolicy))` |
| `__policies__` | `<base>__hash__` | سداسي عشري SHA-256 لبايتات السياسة المشفرة |
| `__policies__` | `<base>__hmac__` | سداسي عشري HMAC-SHA256(policyKey, بايتات السياسة المشفرة) |
| `__audit__/scheme/namespace` | UUID الحدث | JSON — حدث المراجعة (Event) |
| `__audit__/scheme/namespace` | `__chain_index__` | JSON — chainIndex |
| `scheme/namespace` | سلسلة المفتاح | msgpack — بنية Secret |
### بنية Secret (msgpack)```go
type Secret struct {
Ciphertext []byte `msgpack:"ct"`
EncryptedMeta []byte `msgpack:"em,omitempty"`
SchemaVersion int `msgpack:"sv"` // always 1
}
يستخدم هيكل Event حقول توجيه بنص عادي منفصلة (Scheme، Namespace) بجانب حقول الحمولة المشفرة (EncScheme، EncNamespace، EncDetails). يتم حساب المجاميع الاختبارية (Checksums) على حقول التوجيه بنص عادي وبايتات EncDetails المشفرة، بحيث يمكن التحقق من سلامة السلسلة على ثلاثة مستويات بدون أي مفتاح:
| المستوى | يملك | يمكنه التحقق |
|---|---|---|
| عام | لا شيء | سلسلة المجموع الاختباري SHA-256 (يكشف العبث والإدراج) |
| حامل مفتاح التدقيق | auditEncKey | السلسلة الكاملة + فك تشفير Scheme/Namespace/Details |
مثال: يتلقى مدقق الامتثال (compliance auditor) فقط auditEncKey. يمكنه التحقق من سلسلة HMAC الكاملة عبر تدوير المفاتيح وقراءة جميع تفاصيل الحدث، لكنه لا يستطيع فك تشفير أي قيم سرية. يمكن للمراقب العام الذي لديه ملف قاعدة البيانات فقط أن يكتشف ما إذا تم تعديل أي حدث أو إدراجه بعد وقوعه.
يتم تخزين ملح KDF كـ SaltStore مشفر بـ msgpack تحت مفتاح البيانات الوصفية salt. كل تدوير للملح يضيف إدخال SaltEntry جديد ويقدم CurrentVersion. يتم الاحتفاظ بالإدخالات القديمة كمسار تدقيق. يتم تخزين SaltStore بدون تشفير — انظر قرارات الأمان.
يكتب Rotate سجل WAL قبل لمس أي سجل. يحمل WAL WrappedOldKey: المفتاح الرئيسي قبل التدوير المشفر بالمفتاح الرئيسي الجديد. بعد الانهيار، تختفي عبارة المرور القديمة؛ WrappedOldKey هو الطريقة الصحيحة الوحيدة لحمل المفتاح القديم عبر الحدود. في UnlockDatabase، عند وجود WAL، يقوم المفتاح الرئيسي الجديد بفك تشفير WrappedOldKey ويستأنف التدوير من مؤشر WAL. يتم تشفير WAL نفسه باستخدام policyEncKey.
كل عملية مهمة تُلحق حدثًا واضح التلاعب (tamper-evident) بسلسلة تدقيق الدلو. تعتمد سلامة السلسلة على آليتين.
المجموع الاختباري. SHA-256 على prevChecksum، ID، BucketID، Scheme، Namespace، EncDetails، EventType، وTimestamp. استخدام Scheme/Namespace كنص عادي (محفوظ دائمًا بجانب النماذج المشفرة) يضمن أن المجموع الاختباري مستقر عبر مسارات التحميل. يوفر EncDetails سلامة على الحمولة المشفرة.
HMAC. HMAC-SHA256 على جميع الحقول بما في ذلك Seq. المهاجم الذي يمكنه الكتابة إلى قاعدة البيانات لكنه لا يعرف مفتاح التدقيق لا يمكنه إنتاج HMAC صالح. يتحقق VerifyIntegrity من كلا الطبقتين لكل حدث.
حدود حقبة تدوير المفاتيح. في Rotate، يتم إلحاق حدث نقطة تفتيش (checkpoint event) بكل سلسلة نشطة تحمل بصمات كل من مفتاح التدقيق الصادر والوارد. يتم توقيع نقطة التفتيش بالمفتاح الصادر. يمكن لمدققي التدقيق الذين يحملون أي مفتاح حقبة استعادة مفاتيح الحقبة اللاحقة من حقل wrapped_new_key والتحقق من استمرارية HMAC عبر السلسلة الكاملة.
التقليم التلقائي. عند تعيين AuditPruneInterval في Config، يعمل jack.Scheduler بشكل دوري ويستدعي PruneEvents على كل دلو مسجل. لا يتم تقليم دلاء LevelHSM و LevelRemote أبدًا بغض النظر عن هذا الإعداد.
Jack هي مكتبة إشراف على العمليات اختيارية. عند توفير JackConfig عبر WithJack، يقوم keeper بتنشيط المكونات الخلفية تلقائيًا:
LevelAdminWrapped بعد AutoLockInterval. تبقى دلاء LevelPasswordOnly مفتوحة حتى تستمر المهام الخلفية دون انقطاع. نمط قفل الكتابة الواحد داخل مهمة الحلقة يلغي حالة السباق RUnlock→Lock الموجودة في التصاميم السابقة.LevelAdminWrapped.jack.Doctor.PruneEvents دوري على جميع الدلاء غير HSM.إذا لم يتم توفير JackConfig، يعمل keeper بدون هذه المهام الخلفية. لا يستدعي Keeper أبدًا pool.Shutdown — دورة حياة التجمع تعود للمتصل.
توفر x/keepcmd عمليات keeper قابلة لإعادة الاستخدام منفصلة عن أي إطار CLI. قم بتضمينه في تطبيقك الخاص للحصول على إدارة أسرار مكتوبة وقابلة للاختبار دون سحب ثنائي CLI.```go
import "github.com/agberohq/keeper/x/keepcmd"
cmds := &keepcmd.Commands{ Store: func() (*keeper.Keeper, error) { return security.KeeperOpen(cfg) // your own config }, Out: keepcmd.PlainOutput{}, NoClose: false, // true in REPL / session contexts }
cmds.List() // all keys: scheme://namespace/key cmds.List("vault") // all keys in scheme vault cmds.List("vault", "system") // all keys in vault://system cmds.Get("vault://system/jwt_secret") cmds.Set("vault://system/jwt_secret", "newsecret", keepcmd.SetOptions{}) cmds.Rotate(newPassphraseBytes) // caller resolved the passphrase — no prompter dependency cmds.RotateSalt(currentPassBytes) // same
`keepcmd` never calls `prompter` or reads from stdin. Passphrase resolution
is entirely the caller's responsibility — this keeps the package safe in
headless server contexts.
`NoClose: true` prevents `Commands` from calling `store.Close()` after each
operation. Use this in REPL / session contexts where one store is shared
across many calls.
---
## x/keephandler
`x/keephandler` mounts keeper HTTP endpoints on any `net/http` mux. No
external router dependency — it uses Go 1.22+ method+pattern routing with
stdlib `http.ServeMux`.```go
import "github.com/agberohq/keeper/x/keephandler"
keephandler.Mount(mux, store,
keephandler.WithPrefix("/api/keeper"),
keephandler.WithGuard(func(w http.ResponseWriter, r *http.Request, route string) bool {
if !acl.Allow(r.Header.Get("X-Principal"), route) {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return false
}
return true
}),
keephandler.WithHooks(
keephandler.Hook{
Route: keephandler.RouteGet,
CaptureBody: false,
After: func(r *http.Request, status int, _ []byte) {
audit.Log(r.Context(), route, status)
},
},
),
keephandler.WithEncoder(func(w http.ResponseWriter, route string, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]any{
"ok": status < 400,
"route": route,
"data": data,
})
}),
keephandler.WithRoutes(func(m *http.ServeMux) {
m.HandleFunc("POST /api/keeper/totp/{user}", myTOTPHandler)
}),
)
BeforeFunc تُرجع (allow bool, err error).
(true, nil) — السماح بمواصلة الطلب.(false, nil) — الإحباط؛ كتب الخطاف بالفعل استجابة كاملة.(false, err) — الإحباط؛ يكتب الإطار استجابة 500 باستخدام err.Error().
يجب ألا يكون الخطاف قد كتب أي شيء إلى w.Hook.CaptureBody bool يتحكم فيما إذا كانت AfterFunc تتلقى نص
الاستجابة. false (القيمة الافتراضية) تكلف غلاف statusWriter خفيف واحد؛
true يخزن النص الكامل في bytes.Buffer لـ AfterFunc — تخصيص
واحد لكل طلب.
يتم تنفيذ الخطافات بترتيب التسجيل. استدعاءات WithHooks المتعددة
تراكمية. فقط أول خطاف مسجل لاسم مسار معين يُستخدم—يتم تجاهل التسجيلات
اللاحقة لنفس المسار.
store, err := keeper.New(keeper.Config{ DBPath: "/var/lib/agbero/keeper.db", AutoLockInterval: 30 * time.Minute, EnableAudit: true, AuditPruneInterval: 24 * time.Hour, AuditPruneKeepLastN: 10_000, AuditPruneOlderThan: 90 * 24 * time.Hour, DBLatencyThreshold: 200 * time.Millisecond, Logger: logger, }, keeper.WithJack(keeper.JackConfig{ Pool: jackPool, Shutdown: jackShutdown, })) defer store.Close()
// Shorthand (wraps DeriveMaster + UnlockDatabase): if err := store.Unlock([]byte(os.Getenv("KEEPER_PASSPHRASE"))); err != nil { log.Fatal(err) // ErrInvalidPassphrase on wrong passphrase }
`UnlockDatabase` يقوم بما يلي بالترتيب:
1. اشتقاق وتفعيل مفتاح توقيع HMAC للتدقيق (audit HMAC signing key)
2. اشتقاق وتفعيل مفتاح HMAC للسياسة (policy HMAC key)
3. اشتقاق وتفعيل `policyEncKey` و `auditEncKey`
4. مسح وإعادة تحميل `schemeRegistry` (فك تشفير جميع كتل السياسة)
5. استئناف أي سجل WAL للتدوير المتقطع
6. ترقية علامات HMAC للسياسة
7. تغذية جميع مفاتيح DEK لحاوية `LevelPasswordOnly` في المغلف (Envelope)
8. بدء المهام الخلفية (حلقة الترحيل، القفل التلقائي، مرضى الصحة)
### حاوية `LevelPasswordOnly` — دورة الحياة الكاملة```go
err := store.CreateBucket("vault", "system", keeper.LevelPasswordOnly, "init")
store.Set("vault://system/jwt_secret", []byte("supersecret"))
val, err := store.Get("vault://system/jwt_secret")
// Namespaced convenience wrappers
store.SetNamespaced("admin", "jwt_secret", secretBytes)
val, err = store.GetNamespaced("admin", "jwt_secret")
err := store.CreateBucket("finance", "payroll", keeper.LevelAdminWrapped, "ops-team") err = store.AddAdminToPolicy("finance", "payroll", "alice", []byte("alicepass"))
store.SetNamespacedFull("finance", "payroll", "salary_key", []byte("AES256..."))
store.LockBucket("finance", "payroll") err = store.UnlockBucket("finance", "payroll", "bob", []byte("bobpass")) // ErrAuthFailed — does not distinguish wrong password from unknown admin (CWE-204)
err = store.RevokeAdmin("finance", "payroll", "alice") err = store.RotateAdminWrappedDEK("finance", "payroll", "bob", []byte("bobpass"))
needs, err := store.NeedsAdminRekey("finance", "payroll")
### LevelHSM / LevelRemote الدلاء```go
import (
"github.com/agberohq/keeper/pkg/hsm"
"github.com/agberohq/keeper/pkg/remote"
)
// SoftHSM — testing only
provider, _ := hsm.NewSoftHSM()
store.RegisterHSMProvider("secure", "keys", provider)
store.CreateBucket("secure", "keys", keeper.LevelHSM, "ops")
// Vault Transit
cfg := remote.VaultTransit("https://vault.corp:8200", vaultToken, "my-key")
cfg.TLSClientCert = "/etc/keeper/client.crt"
cfg.TLSClientKey = "/etc/keeper/client.key"
provider, _ = remote.New(cfg)
store.RegisterHSMProvider("tenant", "secrets", provider)
store.CreateBucket("tenant", "secrets", keeper.LevelRemote, "ops")
// Export the audit encryption key to allow a third-party auditor to decrypt // event details without access to the master passphrase. auditKey, err := store.ExportAuditKey() defer zero.Bytes(auditKey)
events, err := auditStore.LoadChain("vault", "system", auditKey)
### تدوير المفاتيح```go
// Rotate passphrase — crash-safe WAL, resumes on next Unlock if interrupted
store.Rotate([]byte("new-passphrase"))
// Rotate KDF salt — re-derives master key, re-encrypts LevelPasswordOnly
store.RotateSalt([]byte("current-passphrase"))
err := store.CompareAndSwapNamespacedFull("vault", "system", "counter", []byte("old"), []byte("new")) // ErrCASConflict if current value does not match old
### نسخ احتياطي```go
f, _ := os.Create("keeper.db.bak")
info, err := store.Backup(f)
// info.Bytes, info.Timestamp, info.DBPath
جميع الأخطاء الحارسة تعمل مع errors.Is و errors.As. يتم التقاط تتبعات المكدس عند نقطة الإنشاء عبر github.com/olekukonko/errors.
ErrAuthFailed يوحد جميع حالات فشل UnlockBucket (CWE-204 / CVSS 5.3). سواء كان معرف مدير غير معروف أو كلمة مرور خاطئة، يتم إرجاع ErrAuthFailed. هذا يمنع تعداد معرفات المديرين عن طريق التوقيت أو مقارنة سلسلة الخطأ. يحتفظ RevokeAdmin بـ ErrAdminNotFound لأنه عملية إدارية على مخزن تم فتح قفله بالفعل. تم حذف المقارنة ذات الوقت الثابت لوجود معرف المدير عمدًا. المهاجم الذي يمكنه قياس اختلافات أقل من ميكروثانية في عمليات البحث عن جرافات bbolt سيحتاج إلى وصول إلى نظام الملفات المحلي — وعندها يمكنه قراءة جرافة السياسة مباشرة. يفترض نموذج التهديد أن ملف قاعدة البيانات قد يكون مخترقًا؛ الدفاع عن التوقيت ضد التعداد عن بُعد هو الاهتمام الأساسي.
Argon2id يهيمن على التوقيت. يستغرق Argon2id من 200 إلى 500 مللي ثانية على الأجهزة النموذجية. اختلافات المقارنة بعد الاشتقاق أصغر بأربعة مراتب أو أكثر ولا يمكن قياسها عن بُعد. لا يتم تطبيق أي معادلة مصطنعة.
استرجاع DEK داخل حدود معاملة CAS. يقوم CompareAndSwapNamespacedFull باسترجاع DEK للجرافة داخل معاملة الكتابة في bbolt، مما يلغي النافذة الزمنية التي يمكن أن يغير فيها Rotate المتزامن DEK بين الاسترجاع والاستخدام.
لا يتم تخزين عبارة المرور أبدًا كسلسلة Go في معالج HTTP. يتم فك ترميز جميع حقول عبارة المرور الثلاثة (passphrase، new_passphrase) من JSON مباشرة إلى []byte عبر استخراج الخريطة الخام، مما يبقي مصفوفة السلاسل الخلفية بعيدًا عن الكومة طويلة العمر. يتم مسخ نسخة []byte باستخدام wipeBytes بعد الاستخدام.
لا يوجد علم --passphrase في واجهة سطر الأوامر. تظهر الأعلام في مخرجات ps وسجل القشرة. تقبل واجهة سطر الأوامر عبارة المرور فقط من متغير البيئة KEEPER_PASSPHRASE أو موجه تفاعلي بدون صدى.
قيم REPL السرية غير مرئية أبدًا. يستخدم set <key> في REPL بدون قيمة مضمنة term.ReadPassword — لا تظهر في التمرير الطرفي، أو سجل القشرة، أو ps. يمكن توفير قيمة مضمنة (set key value) للبيانات غير الحساسة عند الحاجة.
SaltStore غير مشفرة عمدًا. يجب أن يكون ملح KDF قابلاً للقراءة قبل UnlockDatabase لاشتقاق المفتاح الرئيسي. policyEncKey (المستخدمة لتشفير جميع البيانات الوصفية الأخرى) مشتقة بدورها من المفتاح الرئيسي — تشفير الملح بـ policyEncKey سيكون دائريًا. يوفر ملح KDF التفرد، وليس السرية؛ لا توجد قيمة أمنية في تشفيره.
مفاتيح جرافة السياسة مشفرة، وليست نصًا عاديًا. مفاتيح السياسة على القرص هي hex(SHA-256("scheme:namespace"))[:32] — 128 بت من مساحة المفاتيح — بدلاً من سلاسل قابلة للقراءة. المهاجم غير المتصل الذي يقرأ ملف bbolt لا يمكنه تعداد أسماء الجرافات دون فك تشفير كتل السياسة.
يستخدم تشفير البيانات الوصفية نفس واجهة التشفير للأسرار. تمر جميع عمليات policyEncKey و auditEncKey عبر s.config.NewCipher(key) — نفس واجهة crypt.Cipher التي تم تكوينها لقيم الأسرار. ينتقل اختيار المستخدم للشيفرة (AES-256-GCM لـ FIPS 140، XChaCha20-Poly1305 افتراضيًا) تلقائيًا إلى تشفير السياسة، WAL، والتدقيق. لا يوجد مسار كود يحدد خوارزمية معينة بشكل ثابت.
يتم تخطي جرافات LevelHSM و LevelRemote أثناء تدوير المفتاح الرئيسي. يتخطى reencryptAllWithKey و RotateSalt هذه الجرافات بشكل صريح. DEK يتحكم فيه المزود؛ تدوير ملح المفتاح الرئيسي لا يؤثر عليه.
تدوير آمن للتعطل مع WrappedOldKey. يكتب Rotate WAL قبل لمس أي سجل. يحمل WAL WrappedOldKey: المفتاح الرئيسي قبل التدوير مشفرًا بالمفتاح الرئيسي الجديد. بعد تعطل، يقوم UnlockDatabase بفك تشفير WrappedOldKey باستخدام المفتاح الجديد الموثوق ويستأنف التدوير من المؤشر.
| المشغل | عبارة مرور رئيسية | كل شيء |
| الطريقة | المسار | الوصف |
|---|
POST | {prefix}/unlock | فتح المتجر باستخدام عبارة مرور |
POST | {prefix}/lock | قفل المتجر |
GET | {prefix}/status | حالة القفل — آمن للاستعلام دون مصادقة |
GET | {prefix}/keys | سرد جميع المفاتيح السرية |
GET | {prefix}/keys/{key} | استرجاع قيمة سرية |
POST | {prefix}/keys | تخزين سر (JSON أو متعدد الأجزاء) |
DELETE | {prefix}/keys/{key} | حذف سر |
POST | {prefix}/rotate | تدوير عبارة المرور الرئيسية |
POST | {prefix}/rotate/salt | تدوير ملح KDF |
GET | {prefix}/backup | بث لقطة من قاعدة البيانات |
| الخطأ | المعنى |
|---|
ErrStoreLocked | تمت محاولة عملية بينما المخزن مقفل |
ErrInvalidPassphrase | عبارة مرور رئيسية خاطئة |
ErrAuthFailed | أي فشل في UnlockBucket — لا يميز بين كلمة مرور خاطئة ومعرف مدير غير معروف (CWE-204) |
ErrKeyNotFound | مفتاح سري غير موجود |
ErrBucketLocked | لم يتم فتح قفل الجرافة |
ErrPolicyImmutable | سياسة ثانية لجرافة موجودة |
ErrPolicyNotFound | لا توجد سياسة للمخطط/مساحة الأسماء المحددة |
ErrAdminNotFound | معرف المدير غير موجود في السياسة — فقط RevokeAdmin |
ErrHSMProviderNil | تم إنشاء جرافة HSM/عن بعد بدون مزود مسجل |
ErrCheckLatency | تجاوز زمن قراءة قاعدة البيانات DBLatencyThreshold |
ErrCASConflict | القيمة الحالية لا تطابق المتوقعة في CompareAndSwap |
ErrSecurityDowngrade | نقل عبر الجرافات من مستوى أمان أعلى إلى أدنى |
ErrAlreadyUnlocked | تم استدعاء UnlockDatabase على مخزن تم فتح قفله بالفعل |
ErrMasterRequired | تم استدعاء UnlockDatabase مع Master فارغ أو مدمر |
ErrChainBroken | فشل التحقق من سلامة سلسلة التدقيق |
ErrMetadataDecrypt | تعذر فك تشفير البيانات الوصفية المشفرة |
ErrPolicySignature | فشل التحقق من HMAC للسياسة — تم العبث بالسجل |
| الحزمة | الغرض |
|---|
go.etcd.io/bbolt | مخزن قيم-مفاتيح مدمج |
golang.org/x/crypto | Argon2id, XChaCha20-Poly1305, HKDF, scrypt |
github.com/awnumar/memguard | حاوية مفاتيح آمنة في الذاكرة (مفتاح رئيسي، مفاتيح تشفير البيانات) |
github.com/vmihailenco/msgpack/v5 | تسلسل ثنائي للأسرار والسياسات |
github.com/olekukonko/jack | إشراف على العمليات (دمج Jack اختياري) |
github.com/olekukonko/ll | تسجيل منظم |
github.com/olekukonko/errors | أخطاء حارسة مع تتبعات المكدس |
github.com/olekukonko/zero | تصفير آمن لشرائح البايت |
github.com/olekukonko/prompter | مطالبات طرفية بدون صدى (سطر الأوامر فقط) |
github.com/integrii/flaggy | تحليل أعلام سطر الأوامر (cmd/keeper فقط) |
golang.org/x/term | كشف TTY وقراءة كلمة المرور الخام (سطر الأوامر فقط) |