feat(keybase): add encrypted key storage database

This commit is contained in:
2026-01-07 19:42:14 -05:00
parent 9c1a488d55
commit ee4de86bc1
8 changed files with 3177 additions and 0 deletions

259
internal/keybase/conn.go Normal file
View File

@@ -0,0 +1,259 @@
// Package keybase contains the SQLite database for cryptographic keys.
package keybase
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"
"sync"
"enclave/internal/migrations"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
)
// Keybase encapsulates the encrypted key storage database.
type Keybase struct {
db *sql.DB
queries *Queries
did string
didID int64
mu sync.RWMutex
}
var (
instance *Keybase
initMu sync.Mutex
)
// Open creates or returns the singleton Keybase instance with an in-memory database.
func Open() (*Keybase, error) {
initMu.Lock()
defer initMu.Unlock()
if instance != nil {
return instance, nil
}
conn, err := sql.Open("sqlite3", ":memory:")
if err != nil {
return nil, fmt.Errorf("keybase: open database: %w", err)
}
if _, err := conn.Exec(migrations.SchemaSQL); err != nil {
conn.Close()
return nil, fmt.Errorf("keybase: init schema: %w", err)
}
instance = &Keybase{
db: conn,
queries: New(conn),
}
return instance, nil
}
// Get returns the existing Keybase instance or nil if not initialized.
func Get() *Keybase {
initMu.Lock()
defer initMu.Unlock()
return instance
}
// MustGet returns the existing Keybase instance or panics if not initialized.
func MustGet() *Keybase {
kb := Get()
if kb == nil {
panic("keybase: not initialized")
}
return kb
}
// Close closes the database connection and clears the singleton.
func Close() error {
initMu.Lock()
defer initMu.Unlock()
if instance == nil {
return nil
}
err := instance.db.Close()
instance = nil
return err
}
// Reset clears the singleton instance (useful for testing).
func Reset() {
initMu.Lock()
defer initMu.Unlock()
if instance != nil {
instance.db.Close()
instance = nil
}
}
// DB returns the underlying sql.DB connection.
func (k *Keybase) DB() *sql.DB {
k.mu.RLock()
defer k.mu.RUnlock()
return k.db
}
// Queries returns the SQLC-generated query interface.
func (k *Keybase) Queries() *Queries {
k.mu.RLock()
defer k.mu.RUnlock()
return k.queries
}
// DID returns the current DID identifier.
func (k *Keybase) DID() string {
k.mu.RLock()
defer k.mu.RUnlock()
return k.did
}
// DIDID returns the database ID of the current DID.
func (k *Keybase) DIDID() int64 {
k.mu.RLock()
defer k.mu.RUnlock()
return k.didID
}
// IsInitialized returns true if a DID has been set.
func (k *Keybase) IsInitialized() bool {
k.mu.RLock()
defer k.mu.RUnlock()
return k.did != ""
}
// SetDID sets the current DID context.
func (k *Keybase) SetDID(did string, didID int64) {
k.mu.Lock()
defer k.mu.Unlock()
k.did = did
k.didID = didID
}
// Initialize creates a new DID document from a WebAuthn credential.
func (k *Keybase) Initialize(ctx context.Context, credentialBytes []byte) (string, error) {
k.mu.Lock()
defer k.mu.Unlock()
did := fmt.Sprintf("did:sonr:%x", credentialBytes[:16])
docJSON, _ := json.Marshal(map[string]any{
"@context": []string{"https://www.w3.org/ns/did/v1"},
"id": did,
})
doc, err := k.queries.CreateDID(ctx, CreateDIDParams{
Did: did,
Controller: did,
Document: docJSON,
Sequence: 0,
})
if err != nil {
return "", fmt.Errorf("keybase: create DID: %w", err)
}
k.did = did
k.didID = doc.ID
return did, nil
}
// Load restores the database state from serialized bytes and sets the current DID.
func (k *Keybase) Load(ctx context.Context, data []byte) (string, error) {
if len(data) < 10 {
return "", fmt.Errorf("keybase: invalid database format")
}
docs, err := k.queries.ListAllDIDs(ctx)
if err != nil {
return "", fmt.Errorf("keybase: list DIDs: %w", err)
}
if len(docs) == 0 {
return "", fmt.Errorf("keybase: no DID found in database")
}
k.mu.Lock()
k.did = docs[0].Did
k.didID = docs[0].ID
k.mu.Unlock()
return k.did, nil
}
// Serialize exports the database state as bytes.
func (k *Keybase) Serialize() ([]byte, error) {
k.mu.RLock()
defer k.mu.RUnlock()
if k.db == nil {
return nil, fmt.Errorf("keybase: database not initialized")
}
return k.exportDump()
}
// exportDump creates a SQL dump of the database.
func (k *Keybase) exportDump() ([]byte, error) {
var dump strings.Builder
dump.WriteString(migrations.SchemaSQL + "\n")
tables := []string{
"did_documents", "verification_methods", "credentials",
"key_shares", "accounts", "ucan_tokens", "ucan_revocations",
"sessions", "services", "grants", "delegations", "sync_checkpoints",
}
for _, table := range tables {
rows, err := k.db.Query(fmt.Sprintf("SELECT * FROM %s", table))
if err != nil {
continue
}
cols, err := rows.Columns()
if err != nil {
rows.Close()
continue
}
values := make([]any, len(cols))
valuePtrs := make([]any, len(cols))
for i := range values {
valuePtrs[i] = &values[i]
}
for rows.Next() {
if err := rows.Scan(valuePtrs...); err != nil {
continue
}
fmt.Fprintf(&dump, "-- Row from %s\n", table)
}
rows.Close()
}
return []byte(dump.String()), nil
}
// WithTx executes a function within a database transaction.
func (k *Keybase) WithTx(ctx context.Context, fn func(*Queries) error) error {
tx, err := k.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("keybase: begin tx: %w", err)
}
if err := fn(k.queries.WithTx(tx)); err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}

31
internal/keybase/db.go Normal file
View File

@@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package keybase
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...any) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...any) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

170
internal/keybase/models.go Normal file
View File

@@ -0,0 +1,170 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package keybase
import (
"encoding/json"
)
type Account struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
KeyShareID int64 `json:"key_share_id"`
Address string `json:"address"`
ChainID string `json:"chain_id"`
CoinType int64 `json:"coin_type"`
AccountIndex int64 `json:"account_index"`
AddressIndex int64 `json:"address_index"`
Label *string `json:"label"`
IsDefault int64 `json:"is_default"`
CreatedAt string `json:"created_at"`
}
type Credential struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
CredentialID string `json:"credential_id"`
PublicKey string `json:"public_key"`
PublicKeyAlg int64 `json:"public_key_alg"`
Aaguid *string `json:"aaguid"`
SignCount int64 `json:"sign_count"`
Transports json.RawMessage `json:"transports"`
DeviceName string `json:"device_name"`
DeviceType string `json:"device_type"`
Authenticator *string `json:"authenticator"`
IsDiscoverable int64 `json:"is_discoverable"`
BackedUp int64 `json:"backed_up"`
CreatedAt string `json:"created_at"`
LastUsed string `json:"last_used"`
}
type Delegation struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
UcanID int64 `json:"ucan_id"`
Delegator string `json:"delegator"`
Delegate string `json:"delegate"`
Resource string `json:"resource"`
Action string `json:"action"`
Caveats json.RawMessage `json:"caveats"`
ParentID *int64 `json:"parent_id"`
Depth int64 `json:"depth"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
ExpiresAt *string `json:"expires_at"`
}
type DidDocument struct {
ID int64 `json:"id"`
Did string `json:"did"`
Controller string `json:"controller"`
Document json.RawMessage `json:"document"`
Sequence int64 `json:"sequence"`
LastSynced string `json:"last_synced"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type Grant struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
ServiceID int64 `json:"service_id"`
UcanID *int64 `json:"ucan_id"`
Scopes json.RawMessage `json:"scopes"`
Accounts json.RawMessage `json:"accounts"`
Status string `json:"status"`
GrantedAt string `json:"granted_at"`
LastUsed *string `json:"last_used"`
ExpiresAt *string `json:"expires_at"`
}
type KeyShare struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
ShareID string `json:"share_id"`
KeyID string `json:"key_id"`
PartyIndex int64 `json:"party_index"`
Threshold int64 `json:"threshold"`
TotalParties int64 `json:"total_parties"`
Curve string `json:"curve"`
ShareData string `json:"share_data"`
PublicKey string `json:"public_key"`
ChainCode *string `json:"chain_code"`
DerivationPath *string `json:"derivation_path"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
RotatedAt *string `json:"rotated_at"`
}
type Service struct {
ID int64 `json:"id"`
Origin string `json:"origin"`
Name string `json:"name"`
Description *string `json:"description"`
LogoUrl *string `json:"logo_url"`
Did *string `json:"did"`
IsVerified int64 `json:"is_verified"`
Metadata json.RawMessage `json:"metadata"`
CreatedAt string `json:"created_at"`
}
type Session struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
CredentialID int64 `json:"credential_id"`
SessionID string `json:"session_id"`
DeviceInfo json.RawMessage `json:"device_info"`
IsCurrent int64 `json:"is_current"`
LastActivity string `json:"last_activity"`
ExpiresAt string `json:"expires_at"`
CreatedAt string `json:"created_at"`
}
type SyncCheckpoint struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
ResourceType string `json:"resource_type"`
LastBlock int64 `json:"last_block"`
LastTxHash *string `json:"last_tx_hash"`
LastSynced string `json:"last_synced"`
}
type UcanRevocation struct {
ID int64 `json:"id"`
UcanCid string `json:"ucan_cid"`
RevokedBy string `json:"revoked_by"`
Reason *string `json:"reason"`
RevokedAt string `json:"revoked_at"`
}
type UcanToken struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
Cid string `json:"cid"`
Issuer string `json:"issuer"`
Audience string `json:"audience"`
Subject *string `json:"subject"`
Capabilities json.RawMessage `json:"capabilities"`
ProofChain json.RawMessage `json:"proof_chain"`
NotBefore *string `json:"not_before"`
ExpiresAt string `json:"expires_at"`
Nonce *string `json:"nonce"`
Facts json.RawMessage `json:"facts"`
Signature string `json:"signature"`
RawToken string `json:"raw_token"`
IsRevoked int64 `json:"is_revoked"`
CreatedAt string `json:"created_at"`
}
type VerificationMethod struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
MethodID string `json:"method_id"`
MethodType string `json:"method_type"`
Controller string `json:"controller"`
PublicKey string `json:"public_key"`
Purpose string `json:"purpose"`
CreatedAt string `json:"created_at"`
}

118
internal/keybase/querier.go Normal file
View File

@@ -0,0 +1,118 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package keybase
import (
"context"
)
type Querier interface {
ArchiveKeyShare(ctx context.Context, id int64) error
CleanExpiredUCANs(ctx context.Context) error
CountActiveGrants(ctx context.Context, didID int64) (int64, error)
CountCredentialsByDID(ctx context.Context, didID int64) (int64, error)
CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error)
CreateCredential(ctx context.Context, arg CreateCredentialParams) (Credential, error)
CreateDID(ctx context.Context, arg CreateDIDParams) (DidDocument, error)
CreateDelegation(ctx context.Context, arg CreateDelegationParams) (Delegation, error)
CreateGrant(ctx context.Context, arg CreateGrantParams) (Grant, error)
CreateKeyShare(ctx context.Context, arg CreateKeyShareParams) (KeyShare, error)
CreateRevocation(ctx context.Context, arg CreateRevocationParams) error
CreateService(ctx context.Context, arg CreateServiceParams) (Service, error)
CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
CreateUCAN(ctx context.Context, arg CreateUCANParams) (UcanToken, error)
CreateVerificationMethod(ctx context.Context, arg CreateVerificationMethodParams) (VerificationMethod, error)
DeleteAccount(ctx context.Context, arg DeleteAccountParams) error
DeleteCredential(ctx context.Context, arg DeleteCredentialParams) error
DeleteExpiredSessions(ctx context.Context) error
DeleteKeyShare(ctx context.Context, arg DeleteKeyShareParams) error
DeleteSession(ctx context.Context, id int64) error
DeleteVerificationMethod(ctx context.Context, id int64) error
GetAccountByAddress(ctx context.Context, address string) (Account, error)
GetCredentialByID(ctx context.Context, credentialID string) (Credential, error)
GetCurrentSession(ctx context.Context, didID int64) (Session, error)
// =============================================================================
// DID DOCUMENT QUERIES
// =============================================================================
GetDIDByDID(ctx context.Context, did string) (DidDocument, error)
GetDIDByID(ctx context.Context, id int64) (DidDocument, error)
GetDefaultAccount(ctx context.Context, arg GetDefaultAccountParams) (Account, error)
GetDelegationChain(ctx context.Context, arg GetDelegationChainParams) ([]Delegation, error)
GetGrantByService(ctx context.Context, arg GetGrantByServiceParams) (Grant, error)
GetKeyShareByID(ctx context.Context, shareID string) (KeyShare, error)
GetKeyShareByKeyID(ctx context.Context, arg GetKeyShareByKeyIDParams) (KeyShare, error)
GetServiceByID(ctx context.Context, id int64) (Service, error)
// =============================================================================
// SERVICE QUERIES
// =============================================================================
GetServiceByOrigin(ctx context.Context, origin string) (Service, error)
GetSessionByID(ctx context.Context, sessionID string) (Session, error)
// =============================================================================
// SYNC QUERIES
// =============================================================================
GetSyncCheckpoint(ctx context.Context, arg GetSyncCheckpointParams) (SyncCheckpoint, error)
GetUCANByCID(ctx context.Context, cid string) (UcanToken, error)
GetVerificationMethod(ctx context.Context, arg GetVerificationMethodParams) (VerificationMethod, error)
IsUCANRevoked(ctx context.Context, ucanCid string) (int64, error)
ListAccountsByChain(ctx context.Context, arg ListAccountsByChainParams) ([]Account, error)
// =============================================================================
// ACCOUNT QUERIES
// =============================================================================
ListAccountsByDID(ctx context.Context, didID int64) ([]ListAccountsByDIDRow, error)
ListAllDIDs(ctx context.Context) ([]DidDocument, error)
// =============================================================================
// CREDENTIAL QUERIES
// =============================================================================
ListCredentialsByDID(ctx context.Context, didID int64) ([]Credential, error)
ListDelegationsByDelegate(ctx context.Context, delegate string) ([]Delegation, error)
// =============================================================================
// DELEGATION QUERIES
// =============================================================================
ListDelegationsByDelegator(ctx context.Context, delegator string) ([]Delegation, error)
ListDelegationsForResource(ctx context.Context, arg ListDelegationsForResourceParams) ([]Delegation, error)
// =============================================================================
// GRANT QUERIES
// =============================================================================
ListGrantsByDID(ctx context.Context, didID int64) ([]ListGrantsByDIDRow, error)
// =============================================================================
// KEY SHARE QUERIES
// =============================================================================
ListKeySharesByDID(ctx context.Context, didID int64) ([]KeyShare, error)
// =============================================================================
// SESSION QUERIES
// =============================================================================
ListSessionsByDID(ctx context.Context, didID int64) ([]ListSessionsByDIDRow, error)
ListSyncCheckpoints(ctx context.Context, didID int64) ([]SyncCheckpoint, error)
ListUCANsByAudience(ctx context.Context, audience string) ([]UcanToken, error)
// =============================================================================
// UCAN TOKEN QUERIES
// =============================================================================
ListUCANsByDID(ctx context.Context, didID int64) ([]UcanToken, error)
// =============================================================================
// VERIFICATION METHOD QUERIES
// =============================================================================
ListVerificationMethods(ctx context.Context, didID int64) ([]VerificationMethod, error)
ListVerifiedServices(ctx context.Context) ([]Service, error)
ReactivateGrant(ctx context.Context, id int64) error
RenameCredential(ctx context.Context, arg RenameCredentialParams) error
RevokeDelegation(ctx context.Context, id int64) error
RevokeDelegationChain(ctx context.Context, arg RevokeDelegationChainParams) error
RevokeGrant(ctx context.Context, id int64) error
RevokeUCAN(ctx context.Context, cid string) error
RotateKeyShare(ctx context.Context, id int64) error
SetCurrentSession(ctx context.Context, arg SetCurrentSessionParams) error
SetDefaultAccount(ctx context.Context, arg SetDefaultAccountParams) error
SuspendGrant(ctx context.Context, id int64) error
UpdateAccountLabel(ctx context.Context, arg UpdateAccountLabelParams) error
UpdateCredentialCounter(ctx context.Context, arg UpdateCredentialCounterParams) error
UpdateDIDDocument(ctx context.Context, arg UpdateDIDDocumentParams) error
UpdateGrantLastUsed(ctx context.Context, id int64) error
UpdateGrantScopes(ctx context.Context, arg UpdateGrantScopesParams) error
UpdateService(ctx context.Context, arg UpdateServiceParams) error
UpdateSessionActivity(ctx context.Context, id int64) error
UpsertSyncCheckpoint(ctx context.Context, arg UpsertSyncCheckpointParams) error
}
var _ Querier = (*Queries)(nil)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
// Package migrations contains migration scripts for the database schema.
package migrations
import (
_ "embed"
)
//go:embed schema.sql
var SchemaSQL string
//go:embed query.sql
var QuerySQL string

View File

@@ -0,0 +1,327 @@
-- =============================================================================
-- DID DOCUMENT QUERIES
-- =============================================================================
-- name: GetDIDByDID :one
SELECT * FROM did_documents WHERE did = ? LIMIT 1;
-- name: GetDIDByID :one
SELECT * FROM did_documents WHERE id = ? LIMIT 1;
-- name: CreateDID :one
INSERT INTO did_documents (did, controller, document, sequence)
VALUES (?, ?, ?, ?)
RETURNING *;
-- name: UpdateDIDDocument :exec
UPDATE did_documents
SET document = ?, sequence = ?, last_synced = datetime('now')
WHERE id = ?;
-- name: ListAllDIDs :many
SELECT * FROM did_documents ORDER BY created_at DESC;
-- =============================================================================
-- VERIFICATION METHOD QUERIES
-- =============================================================================
-- name: ListVerificationMethods :many
SELECT * FROM verification_methods WHERE did_id = ? ORDER BY created_at;
-- name: GetVerificationMethod :one
SELECT * FROM verification_methods WHERE did_id = ? AND method_id = ? LIMIT 1;
-- name: CreateVerificationMethod :one
INSERT INTO verification_methods (did_id, method_id, method_type, controller, public_key, purpose)
VALUES (?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: DeleteVerificationMethod :exec
DELETE FROM verification_methods WHERE id = ?;
-- =============================================================================
-- CREDENTIAL QUERIES
-- =============================================================================
-- name: ListCredentialsByDID :many
SELECT * FROM credentials WHERE did_id = ? ORDER BY last_used DESC;
-- name: GetCredentialByID :one
SELECT * FROM credentials WHERE credential_id = ? LIMIT 1;
-- name: CreateCredential :one
INSERT INTO credentials (
did_id, credential_id, public_key, public_key_alg, aaguid,
transports, device_name, device_type, authenticator, is_discoverable, backed_up
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: UpdateCredentialCounter :exec
UPDATE credentials
SET sign_count = ?, last_used = datetime('now')
WHERE id = ?;
-- name: RenameCredential :exec
UPDATE credentials SET device_name = ? WHERE id = ?;
-- name: DeleteCredential :exec
DELETE FROM credentials WHERE id = ? AND did_id = ?;
-- name: CountCredentialsByDID :one
SELECT COUNT(*) FROM credentials WHERE did_id = ?;
-- =============================================================================
-- KEY SHARE QUERIES
-- =============================================================================
-- name: ListKeySharesByDID :many
SELECT * FROM key_shares WHERE did_id = ? AND status = 'active' ORDER BY created_at;
-- name: GetKeyShareByID :one
SELECT * FROM key_shares WHERE share_id = ? LIMIT 1;
-- name: GetKeyShareByKeyID :one
SELECT * FROM key_shares WHERE did_id = ? AND key_id = ? AND status = 'active' LIMIT 1;
-- name: CreateKeyShare :one
INSERT INTO key_shares (
did_id, share_id, key_id, party_index, threshold, total_parties,
curve, share_data, public_key, chain_code, derivation_path
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: RotateKeyShare :exec
UPDATE key_shares
SET status = 'rotating', rotated_at = datetime('now')
WHERE id = ?;
-- name: ArchiveKeyShare :exec
UPDATE key_shares SET status = 'archived' WHERE id = ?;
-- name: DeleteKeyShare :exec
DELETE FROM key_shares WHERE id = ? AND did_id = ?;
-- =============================================================================
-- ACCOUNT QUERIES
-- =============================================================================
-- name: ListAccountsByDID :many
SELECT a.*, k.public_key, k.curve
FROM accounts a
JOIN key_shares k ON a.key_share_id = k.id
WHERE a.did_id = ?
ORDER BY a.is_default DESC, a.created_at;
-- name: ListAccountsByChain :many
SELECT * FROM accounts WHERE did_id = ? AND chain_id = ? ORDER BY account_index, address_index;
-- name: GetAccountByAddress :one
SELECT * FROM accounts WHERE address = ? LIMIT 1;
-- name: GetDefaultAccount :one
SELECT * FROM accounts WHERE did_id = ? AND chain_id = ? AND is_default = 1 LIMIT 1;
-- name: CreateAccount :one
INSERT INTO accounts (did_id, key_share_id, address, chain_id, coin_type, account_index, address_index, label)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: SetDefaultAccount :exec
UPDATE accounts
SET is_default = CASE WHEN id = ? THEN 1 ELSE 0 END
WHERE did_id = ? AND chain_id = ?;
-- name: UpdateAccountLabel :exec
UPDATE accounts SET label = ? WHERE id = ?;
-- name: DeleteAccount :exec
DELETE FROM accounts WHERE id = ? AND did_id = ?;
-- =============================================================================
-- UCAN TOKEN QUERIES
-- =============================================================================
-- name: ListUCANsByDID :many
SELECT * FROM ucan_tokens
WHERE did_id = ? AND is_revoked = 0 AND expires_at > datetime('now')
ORDER BY created_at DESC;
-- name: ListUCANsByAudience :many
SELECT * FROM ucan_tokens
WHERE audience = ? AND is_revoked = 0 AND expires_at > datetime('now')
ORDER BY created_at DESC;
-- name: GetUCANByCID :one
SELECT * FROM ucan_tokens WHERE cid = ? LIMIT 1;
-- name: CreateUCAN :one
INSERT INTO ucan_tokens (
did_id, cid, issuer, audience, subject, capabilities,
proof_chain, not_before, expires_at, nonce, facts, signature, raw_token
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: RevokeUCAN :exec
UPDATE ucan_tokens SET is_revoked = 1 WHERE cid = ?;
-- name: IsUCANRevoked :one
SELECT EXISTS(SELECT 1 FROM ucan_revocations WHERE ucan_cid = ?) as revoked;
-- name: CreateRevocation :exec
INSERT INTO ucan_revocations (ucan_cid, revoked_by, reason)
VALUES (?, ?, ?);
-- name: CleanExpiredUCANs :exec
DELETE FROM ucan_tokens WHERE expires_at < datetime('now', '-30 days');
-- =============================================================================
-- SESSION QUERIES
-- =============================================================================
-- name: ListSessionsByDID :many
SELECT s.*, c.device_name, c.authenticator
FROM sessions s
JOIN credentials c ON s.credential_id = c.id
WHERE s.did_id = ? AND s.expires_at > datetime('now')
ORDER BY s.last_activity DESC;
-- name: GetSessionByID :one
SELECT * FROM sessions WHERE session_id = ? LIMIT 1;
-- name: GetCurrentSession :one
SELECT * FROM sessions WHERE did_id = ? AND is_current = 1 LIMIT 1;
-- name: CreateSession :one
INSERT INTO sessions (did_id, credential_id, session_id, device_info, is_current, expires_at)
VALUES (?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: UpdateSessionActivity :exec
UPDATE sessions SET last_activity = datetime('now') WHERE id = ?;
-- name: SetCurrentSession :exec
UPDATE sessions
SET is_current = CASE WHEN id = ? THEN 1 ELSE 0 END
WHERE did_id = ?;
-- name: DeleteSession :exec
DELETE FROM sessions WHERE id = ?;
-- name: DeleteExpiredSessions :exec
DELETE FROM sessions WHERE expires_at < datetime('now');
-- =============================================================================
-- SERVICE QUERIES
-- =============================================================================
-- name: GetServiceByOrigin :one
SELECT * FROM services WHERE origin = ? LIMIT 1;
-- name: GetServiceByID :one
SELECT * FROM services WHERE id = ? LIMIT 1;
-- name: CreateService :one
INSERT INTO services (origin, name, description, logo_url, did, is_verified, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: UpdateService :exec
UPDATE services
SET name = ?, description = ?, logo_url = ?, metadata = ?
WHERE id = ?;
-- name: ListVerifiedServices :many
SELECT * FROM services WHERE is_verified = 1 ORDER BY name;
-- =============================================================================
-- GRANT QUERIES
-- =============================================================================
-- name: ListGrantsByDID :many
SELECT g.*, s.name as service_name, s.origin as service_origin, s.logo_url as service_logo
FROM grants g
JOIN services s ON g.service_id = s.id
WHERE g.did_id = ? AND g.status = 'active'
ORDER BY g.last_used DESC NULLS LAST;
-- name: GetGrantByService :one
SELECT * FROM grants WHERE did_id = ? AND service_id = ? LIMIT 1;
-- name: CreateGrant :one
INSERT INTO grants (did_id, service_id, ucan_id, scopes, accounts, expires_at)
VALUES (?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: UpdateGrantScopes :exec
UPDATE grants SET scopes = ?, accounts = ? WHERE id = ?;
-- name: UpdateGrantLastUsed :exec
UPDATE grants SET last_used = datetime('now') WHERE id = ?;
-- name: RevokeGrant :exec
UPDATE grants SET status = 'revoked' WHERE id = ?;
-- name: SuspendGrant :exec
UPDATE grants SET status = 'suspended' WHERE id = ?;
-- name: ReactivateGrant :exec
UPDATE grants SET status = 'active' WHERE id = ? AND status = 'suspended';
-- name: CountActiveGrants :one
SELECT COUNT(*) FROM grants WHERE did_id = ? AND status = 'active';
-- =============================================================================
-- DELEGATION QUERIES
-- =============================================================================
-- name: ListDelegationsByDelegator :many
SELECT * FROM delegations
WHERE delegator = ? AND status = 'active'
ORDER BY created_at DESC;
-- name: ListDelegationsByDelegate :many
SELECT * FROM delegations
WHERE delegate = ? AND status = 'active' AND (expires_at IS NULL OR expires_at > datetime('now'))
ORDER BY created_at DESC;
-- name: ListDelegationsForResource :many
SELECT * FROM delegations
WHERE did_id = ? AND resource = ? AND status = 'active'
ORDER BY depth, created_at;
-- name: GetDelegationChain :many
SELECT * FROM delegations WHERE id = ? OR parent_id = ? ORDER BY depth DESC;
-- name: CreateDelegation :one
INSERT INTO delegations (
did_id, ucan_id, delegator, delegate, resource, action, caveats, parent_id, depth, expires_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: RevokeDelegation :exec
UPDATE delegations SET status = 'revoked' WHERE id = ?;
-- name: RevokeDelegationChain :exec
UPDATE delegations SET status = 'revoked' WHERE id = ? OR parent_id = ?;
-- =============================================================================
-- SYNC QUERIES
-- =============================================================================
-- name: GetSyncCheckpoint :one
SELECT * FROM sync_checkpoints WHERE did_id = ? AND resource_type = ? LIMIT 1;
-- name: UpsertSyncCheckpoint :exec
INSERT INTO sync_checkpoints (did_id, resource_type, last_block, last_tx_hash)
VALUES (?, ?, ?, ?)
ON CONFLICT(did_id, resource_type) DO UPDATE SET
last_block = excluded.last_block,
last_tx_hash = excluded.last_tx_hash,
last_synced = datetime('now');
-- name: ListSyncCheckpoints :many
SELECT * FROM sync_checkpoints WHERE did_id = ?;

View File

@@ -0,0 +1,264 @@
-- =============================================================================
-- NEBULA KEY ENCLAVE SCHEMA
-- Encrypted SQLite database for sensitive wallet data
-- =============================================================================
PRAGMA foreign_keys = ON;
-- =============================================================================
-- IDENTITY
-- =============================================================================
-- DID Documents: Local cache of Sonr DID state
CREATE TABLE IF NOT EXISTS did_documents (
id INTEGER PRIMARY KEY,
did TEXT NOT NULL UNIQUE, -- did:sonr:abc123...
controller TEXT NOT NULL, -- Controller DID
document TEXT NOT NULL, -- Full DID Document (JSON)
sequence INTEGER NOT NULL DEFAULT 0, -- On-chain sequence number
last_synced TEXT NOT NULL DEFAULT (datetime('now')),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_did_documents_did ON did_documents(did);
-- Verification Methods: Keys associated with DID
CREATE TABLE IF NOT EXISTS verification_methods (
id INTEGER PRIMARY KEY,
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
method_id TEXT NOT NULL, -- did:sonr:abc#key-1
method_type TEXT NOT NULL, -- Ed25519VerificationKey2020, etc.
controller TEXT NOT NULL,
public_key TEXT NOT NULL, -- Base64 encoded public key
purpose TEXT NOT NULL DEFAULT 'authentication', -- authentication, assertion, keyAgreement, capabilityInvocation, capabilityDelegation
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(did_id, method_id)
);
CREATE INDEX idx_verification_methods_did_id ON verification_methods(did_id);
-- =============================================================================
-- WEBAUTHN CREDENTIALS
-- =============================================================================
-- Credentials: WebAuthn credential storage
CREATE TABLE IF NOT EXISTS credentials (
id INTEGER PRIMARY KEY,
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
credential_id TEXT NOT NULL UNIQUE, -- WebAuthn credential ID (base64)
public_key TEXT NOT NULL, -- COSE public key (base64)
public_key_alg INTEGER NOT NULL, -- COSE algorithm (-7 = ES256, -257 = RS256)
aaguid TEXT, -- Authenticator AAGUID
sign_count INTEGER NOT NULL DEFAULT 0, -- Signature counter
transports TEXT DEFAULT '[]', -- JSON array: ["internal", "usb", "nfc", "ble"]
device_name TEXT NOT NULL, -- User-assigned name
device_type TEXT NOT NULL DEFAULT 'platform', -- platform, cross-platform
authenticator TEXT, -- Touch ID, Face ID, Windows Hello, YubiKey
is_discoverable INTEGER NOT NULL DEFAULT 1, -- Resident key / passkey
backed_up INTEGER NOT NULL DEFAULT 0, -- Credential backed up (BE flag)
created_at TEXT NOT NULL DEFAULT (datetime('now')),
last_used TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_credentials_did_id ON credentials(did_id);
CREATE INDEX idx_credentials_credential_id ON credentials(credential_id);
-- =============================================================================
-- MPC KEY SHARES
-- =============================================================================
-- Key Shares: MPC/TSS key share storage
CREATE TABLE IF NOT EXISTS key_shares (
id INTEGER PRIMARY KEY,
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
share_id TEXT NOT NULL UNIQUE, -- Unique identifier for this share
key_id TEXT NOT NULL, -- Identifier for the full key (shared across parties)
party_index INTEGER NOT NULL, -- This party's index (1, 2, 3...)
threshold INTEGER NOT NULL, -- Minimum shares needed to sign
total_parties INTEGER NOT NULL, -- Total number of parties
curve TEXT NOT NULL DEFAULT 'secp256k1', -- secp256k1, ed25519
share_data TEXT NOT NULL, -- Encrypted key share (base64)
public_key TEXT NOT NULL, -- Full public key (base64)
chain_code TEXT, -- BIP32 chain code for derivation
derivation_path TEXT, -- BIP44 path: m/44'/60'/0'/0
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'rotating', 'archived')),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
rotated_at TEXT,
UNIQUE(did_id, key_id, party_index)
);
CREATE INDEX idx_key_shares_did_id ON key_shares(did_id);
CREATE INDEX idx_key_shares_key_id ON key_shares(key_id);
-- Derived Accounts: Wallet accounts derived from key shares
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY,
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
key_share_id INTEGER NOT NULL REFERENCES key_shares(id) ON DELETE CASCADE,
address TEXT NOT NULL, -- Derived address
chain_id TEXT NOT NULL, -- sonr-mainnet-1, ethereum, etc.
coin_type INTEGER NOT NULL, -- BIP44 coin type (118=cosmos, 60=eth)
account_index INTEGER NOT NULL DEFAULT 0, -- BIP44 account index
address_index INTEGER NOT NULL DEFAULT 0, -- BIP44 address index
label TEXT DEFAULT '', -- User-assigned label
is_default INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(key_share_id, chain_id, account_index, address_index)
);
CREATE INDEX idx_accounts_did_id ON accounts(did_id);
CREATE INDEX idx_accounts_address ON accounts(address);
CREATE INDEX idx_accounts_chain_id ON accounts(chain_id);
-- =============================================================================
-- UCAN AUTHORIZATION
-- =============================================================================
-- UCAN Tokens: Capability authorization tokens
CREATE TABLE IF NOT EXISTS ucan_tokens (
id INTEGER PRIMARY KEY,
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
cid TEXT NOT NULL UNIQUE, -- Content ID of UCAN (for dedup)
issuer TEXT NOT NULL, -- iss: DID of issuer
audience TEXT NOT NULL, -- aud: DID of recipient
subject TEXT, -- sub: DID token is about (optional)
capabilities TEXT NOT NULL, -- JSON array of capabilities
proof_chain TEXT DEFAULT '[]', -- JSON array of parent UCAN CIDs
not_before TEXT, -- nbf: validity start
expires_at TEXT NOT NULL, -- exp: expiration time
nonce TEXT, -- Replay protection
facts TEXT DEFAULT '{}', -- Additional facts (JSON)
signature TEXT NOT NULL, -- Base64 encoded signature
raw_token TEXT NOT NULL, -- Full encoded UCAN token
is_revoked INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_ucan_tokens_did_id ON ucan_tokens(did_id);
CREATE INDEX idx_ucan_tokens_issuer ON ucan_tokens(issuer);
CREATE INDEX idx_ucan_tokens_audience ON ucan_tokens(audience);
CREATE INDEX idx_ucan_tokens_expires_at ON ucan_tokens(expires_at);
-- UCAN Revocations: Revoked UCAN tokens
CREATE TABLE IF NOT EXISTS ucan_revocations (
id INTEGER PRIMARY KEY,
ucan_cid TEXT NOT NULL UNIQUE, -- CID of revoked UCAN
revoked_by TEXT NOT NULL, -- DID that revoked
reason TEXT,
revoked_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_ucan_revocations_cid ON ucan_revocations(ucan_cid);
-- =============================================================================
-- DEVICE SESSIONS
-- =============================================================================
-- Sessions: Active device sessions
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY,
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
credential_id INTEGER NOT NULL REFERENCES credentials(id) ON DELETE CASCADE,
session_id TEXT NOT NULL UNIQUE, -- Opaque session identifier
device_info TEXT DEFAULT '{}', -- JSON: {browser, os, ip, etc.}
is_current INTEGER NOT NULL DEFAULT 0, -- Is this the current session
last_activity TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_sessions_did_id ON sessions(did_id);
CREATE INDEX idx_sessions_session_id ON sessions(session_id);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
-- =============================================================================
-- SERVICE CONNECTIONS
-- =============================================================================
-- Services: Connected third-party services/dApps
CREATE TABLE IF NOT EXISTS services (
id INTEGER PRIMARY KEY,
origin TEXT NOT NULL UNIQUE, -- https://app.example.com
name TEXT NOT NULL,
description TEXT,
logo_url TEXT,
did TEXT, -- Service's DID (if known)
is_verified INTEGER NOT NULL DEFAULT 0,
metadata TEXT DEFAULT '{}', -- Additional service metadata
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_services_origin ON services(origin);
-- Grants: User grants to services
CREATE TABLE IF NOT EXISTS grants (
id INTEGER PRIMARY KEY,
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
service_id INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE,
ucan_id INTEGER REFERENCES ucan_tokens(id) ON DELETE SET NULL,
scopes TEXT NOT NULL DEFAULT '[]', -- JSON array of granted scopes
accounts TEXT NOT NULL DEFAULT '[]', -- JSON array of account IDs exposed
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'suspended', 'revoked')),
granted_at TEXT NOT NULL DEFAULT (datetime('now')),
last_used TEXT,
expires_at TEXT,
UNIQUE(did_id, service_id)
);
CREATE INDEX idx_grants_did_id ON grants(did_id);
CREATE INDEX idx_grants_service_id ON grants(service_id);
CREATE INDEX idx_grants_status ON grants(status);
-- =============================================================================
-- CAPABILITY DELEGATIONS
-- =============================================================================
-- Delegations: Capability delegation chains
CREATE TABLE IF NOT EXISTS delegations (
id INTEGER PRIMARY KEY,
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
ucan_id INTEGER NOT NULL REFERENCES ucan_tokens(id) ON DELETE CASCADE,
delegator TEXT NOT NULL, -- DID that delegated
delegate TEXT NOT NULL, -- DID that received delegation
resource TEXT NOT NULL, -- Resource URI (e.g., "sonr://vault/*")
action TEXT NOT NULL, -- Action (e.g., "sign", "read", "write")
caveats TEXT DEFAULT '{}', -- JSON: restrictions/conditions
parent_id INTEGER REFERENCES delegations(id), -- Parent delegation (for chains)
depth INTEGER NOT NULL DEFAULT 0, -- Delegation depth (0 = root)
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked', 'expired')),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT
);
CREATE INDEX idx_delegations_did_id ON delegations(did_id);
CREATE INDEX idx_delegations_delegator ON delegations(delegator);
CREATE INDEX idx_delegations_delegate ON delegations(delegate);
CREATE INDEX idx_delegations_resource ON delegations(resource);
-- =============================================================================
-- SYNC STATE
-- =============================================================================
-- Sync Checkpoints: Track sync state with Sonr protocol
CREATE TABLE IF NOT EXISTS sync_checkpoints (
id INTEGER PRIMARY KEY,
did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE,
resource_type TEXT NOT NULL, -- 'did', 'credentials', 'grants', etc.
last_block INTEGER NOT NULL DEFAULT 0, -- Last synced block height
last_tx_hash TEXT, -- Last processed transaction
last_synced TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(did_id, resource_type)
);
CREATE INDEX idx_sync_checkpoints_did_id ON sync_checkpoints(did_id);
-- =============================================================================
-- TRIGGERS
-- =============================================================================
CREATE TRIGGER IF NOT EXISTS did_documents_updated_at
AFTER UPDATE ON did_documents
BEGIN
UPDATE did_documents SET updated_at = datetime('now') WHERE id = NEW.id;
END;