feat(enclave): add WebAuthn PRF key derivation and encryption APIs

This commit is contained in:
2026-01-08 00:28:23 -05:00
parent 96991231d6
commit f6dde77e60
11 changed files with 1878 additions and 0 deletions

219
internal/enclave/crypto.go Normal file
View File

@@ -0,0 +1,219 @@
// Package enclave provides encrypted database operations with WebAuthn PRF key derivation.
package enclave
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"fmt"
"io"
"golang.org/x/crypto/hkdf"
)
const (
// EnclaveSalt is the salt used for HKDF key derivation
EnclaveSalt = "nebula-enclave-v1"
// KeySize is the size of the derived encryption key (256 bits)
KeySize = 32
// NonceSize is the size of the GCM nonce (96 bits)
NonceSize = 12
// AuthTagSize is the size of the GCM authentication tag (128 bits)
AuthTagSize = 16
)
// DeriveEncryptionKey derives a 256-bit encryption key from WebAuthn PRF output using HKDF.
//
// Parameters:
// - prfOutput: The raw PRF output from WebAuthn (typically 32 bytes)
//
// Returns:
// - A 32-byte key suitable for AES-256-GCM encryption
// - An error if key derivation fails
func DeriveEncryptionKey(prfOutput []byte) ([]byte, error) {
if len(prfOutput) == 0 {
return nil, fmt.Errorf("enclave: PRF output cannot be empty")
}
salt := []byte(EnclaveSalt)
info := []byte("database-encryption")
hkdfReader := hkdf.New(sha256.New, prfOutput, salt, info)
key := make([]byte, KeySize)
if _, err := io.ReadFull(hkdfReader, key); err != nil {
return nil, fmt.Errorf("enclave: failed to derive key: %w", err)
}
return key, nil
}
// DeriveKeyWithContext derives an encryption key with additional context binding.
// This allows deriving different keys for different purposes from the same PRF output.
//
// Parameters:
// - prfOutput: The raw PRF output from WebAuthn
// - context: Additional context to bind the key to (e.g., "database", "mpc-share")
func DeriveKeyWithContext(prfOutput []byte, context string) ([]byte, error) {
if len(prfOutput) == 0 {
return nil, fmt.Errorf("enclave: PRF output cannot be empty")
}
if context == "" {
return nil, fmt.Errorf("enclave: context cannot be empty")
}
salt := []byte(EnclaveSalt)
info := []byte(context)
hkdfReader := hkdf.New(sha256.New, prfOutput, salt, info)
key := make([]byte, KeySize)
if _, err := io.ReadFull(hkdfReader, key); err != nil {
return nil, fmt.Errorf("enclave: failed to derive key: %w", err)
}
return key, nil
}
// EncryptedData represents encrypted data with its metadata.
type EncryptedData struct {
// Nonce is the unique nonce used for this encryption (12 bytes)
Nonce []byte `json:"nonce"`
// Ciphertext is the encrypted data including the GCM authentication tag
Ciphertext []byte `json:"ciphertext"`
// Version indicates the encryption scheme version
Version int `json:"version"`
}
// Encrypt encrypts plaintext using AES-256-GCM with the provided key.
//
// Parameters:
// - key: 32-byte encryption key (from DeriveEncryptionKey)
// - plaintext: The data to encrypt
//
// Returns:
// - EncryptedData containing nonce and ciphertext
// - An error if encryption fails
func Encrypt(key, plaintext []byte) (*EncryptedData, error) {
if len(key) != KeySize {
return nil, fmt.Errorf("enclave: invalid key size: got %d, want %d", len(key), KeySize)
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("enclave: failed to create cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("enclave: failed to create GCM: %w", err)
}
nonce := make([]byte, NonceSize)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, fmt.Errorf("enclave: failed to generate nonce: %w", err)
}
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
return &EncryptedData{
Nonce: nonce,
Ciphertext: ciphertext,
Version: 1,
}, nil
}
// Decrypt decrypts ciphertext using AES-256-GCM with the provided key.
//
// Parameters:
// - key: 32-byte encryption key (from DeriveEncryptionKey)
// - data: The EncryptedData to decrypt
//
// Returns:
// - The decrypted plaintext
// - An error if decryption fails (including authentication failure)
func Decrypt(key []byte, data *EncryptedData) ([]byte, error) {
if len(key) != KeySize {
return nil, fmt.Errorf("enclave: invalid key size: got %d, want %d", len(key), KeySize)
}
if data == nil {
return nil, fmt.Errorf("enclave: encrypted data cannot be nil")
}
if len(data.Nonce) != NonceSize {
return nil, fmt.Errorf("enclave: invalid nonce size: got %d, want %d", len(data.Nonce), NonceSize)
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("enclave: failed to create cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("enclave: failed to create GCM: %w", err)
}
plaintext, err := gcm.Open(nil, data.Nonce, data.Ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("enclave: decryption failed (authentication error): %w", err)
}
return plaintext, nil
}
// EncryptBytes is a convenience function that encrypts and returns serialized bytes.
// The format is: version (1 byte) + nonce (12 bytes) + ciphertext (variable)
func EncryptBytes(key, plaintext []byte) ([]byte, error) {
data, err := Encrypt(key, plaintext)
if err != nil {
return nil, err
}
result := make([]byte, 1+NonceSize+len(data.Ciphertext))
result[0] = byte(data.Version)
copy(result[1:1+NonceSize], data.Nonce)
copy(result[1+NonceSize:], data.Ciphertext)
return result, nil
}
// DecryptBytes is a convenience function that decrypts serialized encrypted bytes.
// Expected format: version (1 byte) + nonce (12 bytes) + ciphertext (variable)
func DecryptBytes(key, encryptedBytes []byte) ([]byte, error) {
if len(encryptedBytes) < 1+NonceSize+AuthTagSize {
return nil, fmt.Errorf("enclave: encrypted data too short")
}
version := int(encryptedBytes[0])
if version != 1 {
return nil, fmt.Errorf("enclave: unsupported encryption version: %d", version)
}
data := &EncryptedData{
Version: version,
Nonce: encryptedBytes[1 : 1+NonceSize],
Ciphertext: encryptedBytes[1+NonceSize:],
}
return Decrypt(key, data)
}
// GenerateNonce generates a cryptographically secure random nonce.
func GenerateNonce() ([]byte, error) {
nonce := make([]byte, NonceSize)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, fmt.Errorf("enclave: failed to generate nonce: %w", err)
}
return nonce, nil
}
// SecureZero zeros out a byte slice to prevent sensitive data from remaining in memory.
func SecureZero(b []byte) {
for i := range b {
b[i] = 0
}
}

184
internal/enclave/enclave.go Normal file
View File

@@ -0,0 +1,184 @@
package enclave
import (
"encoding/json"
"fmt"
"enclave/internal/keybase"
)
// Enclave wraps a Keybase with encryption capabilities using WebAuthn PRF-derived keys.
type Enclave struct {
keybase *keybase.Keybase
encryptionKey []byte
}
// Config holds enclave configuration options.
type Config struct {
PRFOutput []byte
}
// New creates a new Enclave with the given PRF output for key derivation.
func New(prfOutput []byte) (*Enclave, error) {
if len(prfOutput) == 0 {
return nil, fmt.Errorf("enclave: PRF output required")
}
key, err := DeriveEncryptionKey(prfOutput)
if err != nil {
return nil, fmt.Errorf("enclave: key derivation failed: %w", err)
}
kb, err := keybase.Open()
if err != nil {
SecureZero(key)
return nil, fmt.Errorf("enclave: failed to open keybase: %w", err)
}
return &Enclave{
keybase: kb,
encryptionKey: key,
}, nil
}
// Keybase returns the underlying keybase instance.
func (e *Enclave) Keybase() *keybase.Keybase {
return e.keybase
}
// DID returns the current DID.
func (e *Enclave) DID() string {
return e.keybase.DID()
}
// IsInitialized returns true if the enclave has been initialized with a DID.
func (e *Enclave) IsInitialized() bool {
return e.keybase.IsInitialized()
}
// SerializeEncrypted exports the database state as encrypted bytes.
func (e *Enclave) SerializeEncrypted() ([]byte, error) {
plaintext, err := e.keybase.Serialize()
if err != nil {
return nil, fmt.Errorf("enclave: serialization failed: %w", err)
}
encrypted, err := EncryptBytes(e.encryptionKey, plaintext)
if err != nil {
SecureZero(plaintext)
return nil, fmt.Errorf("enclave: encryption failed: %w", err)
}
SecureZero(plaintext)
return encrypted, nil
}
// LoadEncrypted loads the database state from encrypted bytes.
func (e *Enclave) LoadEncrypted(encryptedData []byte) error {
plaintext, err := DecryptBytes(e.encryptionKey, encryptedData)
if err != nil {
return fmt.Errorf("enclave: decryption failed: %w", err)
}
defer SecureZero(plaintext)
return e.loadFromPlaintext(plaintext)
}
// loadFromPlaintext parses and executes the SQL statements to restore database state.
func (e *Enclave) loadFromPlaintext(data []byte) error {
if len(data) == 0 {
return fmt.Errorf("enclave: empty data")
}
return e.keybase.RestoreFromDump(data)
}
// Close securely closes the enclave and zeros out sensitive data.
func (e *Enclave) Close() error {
SecureZero(e.encryptionKey)
return keybase.Close()
}
// EncryptedBundle represents a complete encrypted database export.
type EncryptedBundle struct {
Version int `json:"version"`
DID string `json:"did"`
Ciphertext []byte `json:"ciphertext"`
Nonce []byte `json:"nonce"`
}
// Export creates a complete encrypted bundle for storage.
func (e *Enclave) Export() (*EncryptedBundle, error) {
plaintext, err := e.keybase.Serialize()
if err != nil {
return nil, fmt.Errorf("enclave: serialization failed: %w", err)
}
defer SecureZero(plaintext)
encData, err := Encrypt(e.encryptionKey, plaintext)
if err != nil {
return nil, fmt.Errorf("enclave: encryption failed: %w", err)
}
return &EncryptedBundle{
Version: encData.Version,
DID: e.keybase.DID(),
Ciphertext: encData.Ciphertext,
Nonce: encData.Nonce,
}, nil
}
// Import loads an encrypted bundle.
func (e *Enclave) Import(bundle *EncryptedBundle) error {
if bundle == nil {
return fmt.Errorf("enclave: bundle cannot be nil")
}
encData := &EncryptedData{
Version: bundle.Version,
Ciphertext: bundle.Ciphertext,
Nonce: bundle.Nonce,
}
plaintext, err := Decrypt(e.encryptionKey, encData)
if err != nil {
return fmt.Errorf("enclave: decryption failed: %w", err)
}
defer SecureZero(plaintext)
return e.loadFromPlaintext(plaintext)
}
// MarshalBundle serializes an encrypted bundle to JSON.
func (b *EncryptedBundle) Marshal() ([]byte, error) {
return json.Marshal(b)
}
// UnmarshalBundle deserializes an encrypted bundle from JSON.
func UnmarshalBundle(data []byte) (*EncryptedBundle, error) {
var bundle EncryptedBundle
if err := json.Unmarshal(data, &bundle); err != nil {
return nil, fmt.Errorf("enclave: failed to unmarshal bundle: %w", err)
}
return &bundle, nil
}
// FromExisting wraps an existing keybase with encryption capabilities.
func FromExisting(kb *keybase.Keybase, prfOutput []byte) (*Enclave, error) {
if kb == nil {
return nil, fmt.Errorf("enclave: keybase cannot be nil")
}
if len(prfOutput) == 0 {
return nil, fmt.Errorf("enclave: PRF output required")
}
key, err := DeriveEncryptionKey(prfOutput)
if err != nil {
return nil, fmt.Errorf("enclave: key derivation failed: %w", err)
}
return &Enclave{
keybase: kb,
encryptionKey: key,
}, nil
}

View File

@@ -0,0 +1,177 @@
package keybase
import (
"context"
"fmt"
)
type NewAccountInput struct {
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,omitempty"`
}
func (am *ActionManager) CreateAccount(ctx context.Context, params NewAccountInput) (*AccountResult, error) {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
if am.kb.didID == 0 {
return nil, fmt.Errorf("DID not initialized")
}
var label *string
if params.Label != "" {
label = &params.Label
}
acc, err := am.kb.queries.CreateAccount(ctx, CreateAccountParams{
DidID: am.kb.didID,
KeyShareID: params.KeyShareID,
Address: params.Address,
ChainID: params.ChainID,
CoinType: params.CoinType,
AccountIndex: params.AccountIndex,
AddressIndex: params.AddressIndex,
Label: label,
})
if err != nil {
return nil, fmt.Errorf("create account: %w", err)
}
labelStr := ""
if acc.Label != nil {
labelStr = *acc.Label
}
return &AccountResult{
ID: acc.ID,
Address: acc.Address,
ChainID: acc.ChainID,
CoinType: acc.CoinType,
AccountIndex: acc.AccountIndex,
AddressIndex: acc.AddressIndex,
Label: labelStr,
IsDefault: acc.IsDefault == 1,
CreatedAt: acc.CreatedAt,
}, nil
}
func (am *ActionManager) ListAccountsByChain(ctx context.Context, chainID string) ([]AccountResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
if am.kb.didID == 0 {
return []AccountResult{}, nil
}
accounts, err := am.kb.queries.ListAccountsByChain(ctx, ListAccountsByChainParams{
DidID: am.kb.didID,
ChainID: chainID,
})
if err != nil {
return nil, fmt.Errorf("list accounts by chain: %w", err)
}
results := make([]AccountResult, len(accounts))
for i, acc := range accounts {
label := ""
if acc.Label != nil {
label = *acc.Label
}
results[i] = AccountResult{
ID: acc.ID,
Address: acc.Address,
ChainID: acc.ChainID,
CoinType: acc.CoinType,
AccountIndex: acc.AccountIndex,
AddressIndex: acc.AddressIndex,
Label: label,
IsDefault: acc.IsDefault == 1,
CreatedAt: acc.CreatedAt,
}
}
return results, nil
}
func (am *ActionManager) GetDefaultAccount(ctx context.Context, chainID string) (*AccountResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
if am.kb.didID == 0 {
return nil, fmt.Errorf("DID not initialized")
}
acc, err := am.kb.queries.GetDefaultAccount(ctx, GetDefaultAccountParams{
DidID: am.kb.didID,
ChainID: chainID,
})
if err != nil {
return nil, fmt.Errorf("get default account: %w", err)
}
label := ""
if acc.Label != nil {
label = *acc.Label
}
return &AccountResult{
ID: acc.ID,
Address: acc.Address,
ChainID: acc.ChainID,
CoinType: acc.CoinType,
AccountIndex: acc.AccountIndex,
AddressIndex: acc.AddressIndex,
Label: label,
IsDefault: acc.IsDefault == 1,
CreatedAt: acc.CreatedAt,
}, nil
}
func (am *ActionManager) SetDefaultAccount(ctx context.Context, accountID int64, chainID string) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
if am.kb.didID == 0 {
return fmt.Errorf("DID not initialized")
}
return am.kb.queries.SetDefaultAccount(ctx, SetDefaultAccountParams{
ID: accountID,
DidID: am.kb.didID,
ChainID: chainID,
})
}
func (am *ActionManager) UpdateAccountLabel(ctx context.Context, accountID int64, label string) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
var labelPtr *string
if label != "" {
labelPtr = &label
}
return am.kb.queries.UpdateAccountLabel(ctx, UpdateAccountLabelParams{
Label: labelPtr,
ID: accountID,
})
}
func (am *ActionManager) DeleteAccount(ctx context.Context, accountID int64) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
if am.kb.didID == 0 {
return fmt.Errorf("DID not initialized")
}
return am.kb.queries.DeleteAccount(ctx, DeleteAccountParams{
ID: accountID,
DidID: am.kb.didID,
})
}

View File

@@ -0,0 +1,155 @@
package keybase
import (
"context"
"encoding/json"
"fmt"
)
type NewCredentialInput struct {
CredentialID string `json:"credential_id"`
PublicKey string `json:"public_key"`
PublicKeyAlg int64 `json:"public_key_alg"`
AAGUID string `json:"aaguid,omitempty"`
Transports []string `json:"transports"`
DeviceName string `json:"device_name"`
DeviceType string `json:"device_type"`
Authenticator string `json:"authenticator,omitempty"`
IsDiscoverable bool `json:"is_discoverable"`
BackedUp bool `json:"backed_up"`
}
func (am *ActionManager) CreateCredential(ctx context.Context, params NewCredentialInput) (*CredentialResult, error) {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
if am.kb.didID == 0 {
return nil, fmt.Errorf("DID not initialized")
}
var aaguid, authenticator *string
if params.AAGUID != "" {
aaguid = &params.AAGUID
}
if params.Authenticator != "" {
authenticator = &params.Authenticator
}
transports, err := json.Marshal(params.Transports)
if err != nil {
return nil, fmt.Errorf("marshal transports: %w", err)
}
var isDiscoverable, backedUp int64
if params.IsDiscoverable {
isDiscoverable = 1
}
if params.BackedUp {
backedUp = 1
}
cred, err := am.kb.queries.CreateCredential(ctx, CreateCredentialParams{
DidID: am.kb.didID,
CredentialID: params.CredentialID,
PublicKey: params.PublicKey,
PublicKeyAlg: params.PublicKeyAlg,
Aaguid: aaguid,
Transports: transports,
DeviceName: params.DeviceName,
DeviceType: params.DeviceType,
Authenticator: authenticator,
IsDiscoverable: isDiscoverable,
BackedUp: backedUp,
})
if err != nil {
return nil, fmt.Errorf("create credential: %w", err)
}
return credentialToResult(&cred), nil
}
func (am *ActionManager) UpdateCredentialCounter(ctx context.Context, credentialID string, signCount int64) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
cred, err := am.kb.queries.GetCredentialByID(ctx, credentialID)
if err != nil {
return fmt.Errorf("get credential: %w", err)
}
return am.kb.queries.UpdateCredentialCounter(ctx, UpdateCredentialCounterParams{
SignCount: signCount,
ID: cred.ID,
})
}
func (am *ActionManager) RenameCredential(ctx context.Context, credentialID string, newName string) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
cred, err := am.kb.queries.GetCredentialByID(ctx, credentialID)
if err != nil {
return fmt.Errorf("get credential: %w", err)
}
return am.kb.queries.RenameCredential(ctx, RenameCredentialParams{
DeviceName: newName,
ID: cred.ID,
})
}
func (am *ActionManager) DeleteCredential(ctx context.Context, credentialID string) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
if am.kb.didID == 0 {
return fmt.Errorf("DID not initialized")
}
cred, err := am.kb.queries.GetCredentialByID(ctx, credentialID)
if err != nil {
return fmt.Errorf("get credential: %w", err)
}
return am.kb.queries.DeleteCredential(ctx, DeleteCredentialParams{
ID: cred.ID,
DidID: am.kb.didID,
})
}
func (am *ActionManager) CountCredentialsByDID(ctx context.Context) (int64, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
if am.kb.didID == 0 {
return 0, nil
}
return am.kb.queries.CountCredentialsByDID(ctx, am.kb.didID)
}
func credentialToResult(cred *Credential) *CredentialResult {
var transports []string
if err := json.Unmarshal(cred.Transports, &transports); err != nil {
transports = []string{}
}
authenticator := ""
if cred.Authenticator != nil {
authenticator = *cred.Authenticator
}
return &CredentialResult{
ID: cred.ID,
CredentialID: cred.CredentialID,
DeviceName: cred.DeviceName,
DeviceType: cred.DeviceType,
Authenticator: authenticator,
Transports: transports,
SignCount: cred.SignCount,
IsDiscoverable: cred.IsDiscoverable == 1,
BackedUp: cred.BackedUp == 1,
CreatedAt: cred.CreatedAt,
LastUsed: cred.LastUsed,
}
}

View File

@@ -0,0 +1,181 @@
package keybase
import (
"context"
"encoding/json"
"fmt"
)
type DelegationResult struct {
ID int64 `json:"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"`
Depth int64 `json:"depth"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
ExpiresAt string `json:"expires_at,omitempty"`
}
type NewDelegationInput struct {
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,omitempty"`
ParentID int64 `json:"parent_id,omitempty"`
Depth int64 `json:"depth"`
ExpiresAt string `json:"expires_at,omitempty"`
}
func (am *ActionManager) CreateDelegation(ctx context.Context, params NewDelegationInput) (*DelegationResult, error) {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
if am.kb.didID == 0 {
return nil, fmt.Errorf("DID not initialized")
}
var parentID *int64
if params.ParentID != 0 {
parentID = &params.ParentID
}
var expiresAt *string
if params.ExpiresAt != "" {
expiresAt = &params.ExpiresAt
}
caveats := params.Caveats
if caveats == nil {
caveats = json.RawMessage(`{}`)
}
d, err := am.kb.queries.CreateDelegation(ctx, CreateDelegationParams{
DidID: am.kb.didID,
UcanID: params.UcanID,
Delegator: params.Delegator,
Delegate: params.Delegate,
Resource: params.Resource,
Action: params.Action,
Caveats: caveats,
ParentID: parentID,
Depth: params.Depth,
ExpiresAt: expiresAt,
})
if err != nil {
return nil, fmt.Errorf("create delegation: %w", err)
}
return delegationToResult(&d), nil
}
func (am *ActionManager) ListDelegationsByDelegator(ctx context.Context, delegator string) ([]DelegationResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
delegations, err := am.kb.queries.ListDelegationsByDelegator(ctx, delegator)
if err != nil {
return nil, fmt.Errorf("list delegations by delegator: %w", err)
}
results := make([]DelegationResult, len(delegations))
for i, d := range delegations {
results[i] = *delegationToResult(&d)
}
return results, nil
}
func (am *ActionManager) ListDelegationsByDelegate(ctx context.Context, delegate string) ([]DelegationResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
delegations, err := am.kb.queries.ListDelegationsByDelegate(ctx, delegate)
if err != nil {
return nil, fmt.Errorf("list delegations by delegate: %w", err)
}
results := make([]DelegationResult, len(delegations))
for i, d := range delegations {
results[i] = *delegationToResult(&d)
}
return results, nil
}
func (am *ActionManager) ListDelegationsForResource(ctx context.Context, resource string) ([]DelegationResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
if am.kb.didID == 0 {
return []DelegationResult{}, nil
}
delegations, err := am.kb.queries.ListDelegationsForResource(ctx, ListDelegationsForResourceParams{
DidID: am.kb.didID,
Resource: resource,
})
if err != nil {
return nil, fmt.Errorf("list delegations for resource: %w", err)
}
results := make([]DelegationResult, len(delegations))
for i, d := range delegations {
results[i] = *delegationToResult(&d)
}
return results, nil
}
func (am *ActionManager) GetDelegationChain(ctx context.Context, delegationID int64) ([]DelegationResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
delegations, err := am.kb.queries.GetDelegationChain(ctx, GetDelegationChainParams{
ID: delegationID,
ParentID: &delegationID,
})
if err != nil {
return nil, fmt.Errorf("get delegation chain: %w", err)
}
results := make([]DelegationResult, len(delegations))
for i, d := range delegations {
results[i] = *delegationToResult(&d)
}
return results, nil
}
func (am *ActionManager) RevokeDelegation(ctx context.Context, delegationID int64) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
return am.kb.queries.RevokeDelegation(ctx, delegationID)
}
func delegationToResult(d *Delegation) *DelegationResult {
expiresAt := ""
if d.ExpiresAt != nil {
expiresAt = *d.ExpiresAt
}
return &DelegationResult{
ID: d.ID,
UcanID: d.UcanID,
Delegator: d.Delegator,
Delegate: d.Delegate,
Resource: d.Resource,
Action: d.Action,
Caveats: d.Caveats,
Depth: d.Depth,
Status: d.Status,
CreatedAt: d.CreatedAt,
ExpiresAt: expiresAt,
}
}

View File

@@ -0,0 +1,181 @@
package keybase
import (
"context"
"encoding/json"
"fmt"
)
type NewGrantInput struct {
ServiceID int64 `json:"service_id"`
UcanID int64 `json:"ucan_id,omitempty"`
Scopes json.RawMessage `json:"scopes"`
Accounts json.RawMessage `json:"accounts"`
ExpiresAt string `json:"expires_at,omitempty"`
}
func (am *ActionManager) CreateGrant(ctx context.Context, params NewGrantInput) (*GrantResult, error) {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
if am.kb.didID == 0 {
return nil, fmt.Errorf("DID not initialized")
}
var ucanID *int64
if params.UcanID != 0 {
ucanID = &params.UcanID
}
var expiresAt *string
if params.ExpiresAt != "" {
expiresAt = &params.ExpiresAt
}
scopes := params.Scopes
if scopes == nil {
scopes = json.RawMessage(`[]`)
}
accounts := params.Accounts
if accounts == nil {
accounts = json.RawMessage(`[]`)
}
g, err := am.kb.queries.CreateGrant(ctx, CreateGrantParams{
DidID: am.kb.didID,
ServiceID: params.ServiceID,
UcanID: ucanID,
Scopes: scopes,
Accounts: accounts,
ExpiresAt: expiresAt,
})
if err != nil {
return nil, fmt.Errorf("create grant: %w", err)
}
svc, err := am.kb.queries.GetServiceByID(ctx, g.ServiceID)
if err != nil {
return nil, fmt.Errorf("get service: %w", err)
}
serviceLogo := ""
if svc.LogoUrl != nil {
serviceLogo = *svc.LogoUrl
}
lastUsed := ""
if g.LastUsed != nil {
lastUsed = *g.LastUsed
}
expires := ""
if g.ExpiresAt != nil {
expires = *g.ExpiresAt
}
return &GrantResult{
ID: g.ID,
ServiceName: svc.Name,
ServiceOrigin: svc.Origin,
ServiceLogo: serviceLogo,
Scopes: g.Scopes,
Accounts: g.Accounts,
Status: g.Status,
GrantedAt: g.GrantedAt,
LastUsed: lastUsed,
ExpiresAt: expires,
}, nil
}
func (am *ActionManager) GetGrantByService(ctx context.Context, serviceID int64) (*GrantResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
if am.kb.didID == 0 {
return nil, fmt.Errorf("DID not initialized")
}
g, err := am.kb.queries.GetGrantByService(ctx, GetGrantByServiceParams{
DidID: am.kb.didID,
ServiceID: serviceID,
})
if err != nil {
return nil, fmt.Errorf("get grant by service: %w", err)
}
svc, err := am.kb.queries.GetServiceByID(ctx, g.ServiceID)
if err != nil {
return nil, fmt.Errorf("get service: %w", err)
}
serviceLogo := ""
if svc.LogoUrl != nil {
serviceLogo = *svc.LogoUrl
}
lastUsed := ""
if g.LastUsed != nil {
lastUsed = *g.LastUsed
}
expires := ""
if g.ExpiresAt != nil {
expires = *g.ExpiresAt
}
return &GrantResult{
ID: g.ID,
ServiceName: svc.Name,
ServiceOrigin: svc.Origin,
ServiceLogo: serviceLogo,
Scopes: g.Scopes,
Accounts: g.Accounts,
Status: g.Status,
GrantedAt: g.GrantedAt,
LastUsed: lastUsed,
ExpiresAt: expires,
}, nil
}
func (am *ActionManager) UpdateGrantScopes(ctx context.Context, grantID int64, scopes, accounts json.RawMessage) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
return am.kb.queries.UpdateGrantScopes(ctx, UpdateGrantScopesParams{
Scopes: scopes,
Accounts: accounts,
ID: grantID,
})
}
func (am *ActionManager) UpdateGrantLastUsed(ctx context.Context, grantID int64) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
return am.kb.queries.UpdateGrantLastUsed(ctx, grantID)
}
func (am *ActionManager) SuspendGrant(ctx context.Context, grantID int64) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
return am.kb.queries.SuspendGrant(ctx, grantID)
}
func (am *ActionManager) ReactivateGrant(ctx context.Context, grantID int64) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
return am.kb.queries.ReactivateGrant(ctx, grantID)
}
func (am *ActionManager) CountActiveGrants(ctx context.Context) (int64, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
if am.kb.didID == 0 {
return 0, nil
}
return am.kb.queries.CountActiveGrants(ctx, am.kb.didID)
}

View File

@@ -0,0 +1,206 @@
package keybase
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
)
type KeyShareResult struct {
ID int64 `json:"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"`
PublicKey string `json:"public_key"`
ChainCode string `json:"chain_code,omitempty"`
DerivationPath string `json:"derivation_path,omitempty"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
RotatedAt string `json:"rotated_at,omitempty"`
}
type NewKeyShareInput struct {
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,omitempty"`
DerivationPath string `json:"derivation_path,omitempty"`
}
func (am *ActionManager) CreateKeyShare(ctx context.Context, params NewKeyShareInput) (*KeyShareResult, error) {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
if am.kb.didID == 0 {
return nil, fmt.Errorf("DID not initialized")
}
shareID := generateShareID()
var chainCode, derivationPath *string
if params.ChainCode != "" {
chainCode = &params.ChainCode
}
if params.DerivationPath != "" {
derivationPath = &params.DerivationPath
}
ks, err := am.kb.queries.CreateKeyShare(ctx, CreateKeyShareParams{
DidID: am.kb.didID,
ShareID: shareID,
KeyID: params.KeyID,
PartyIndex: params.PartyIndex,
Threshold: params.Threshold,
TotalParties: params.TotalParties,
Curve: params.Curve,
ShareData: params.ShareData,
PublicKey: params.PublicKey,
ChainCode: chainCode,
DerivationPath: derivationPath,
})
if err != nil {
return nil, fmt.Errorf("create key share: %w", err)
}
return keyShareToResult(&ks), nil
}
func (am *ActionManager) ListKeyShares(ctx context.Context) ([]KeyShareResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
if am.kb.didID == 0 {
return []KeyShareResult{}, nil
}
shares, err := am.kb.queries.ListKeySharesByDID(ctx, am.kb.didID)
if err != nil {
return nil, fmt.Errorf("list key shares: %w", err)
}
results := make([]KeyShareResult, len(shares))
for i, ks := range shares {
results[i] = *keyShareToResult(&ks)
}
return results, nil
}
func (am *ActionManager) GetKeyShareByID(ctx context.Context, shareID string) (*KeyShareResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
ks, err := am.kb.queries.GetKeyShareByID(ctx, shareID)
if err != nil {
return nil, fmt.Errorf("get key share: %w", err)
}
return keyShareToResult(&ks), nil
}
func (am *ActionManager) GetKeyShareByKeyID(ctx context.Context, keyID string) (*KeyShareResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
if am.kb.didID == 0 {
return nil, fmt.Errorf("DID not initialized")
}
ks, err := am.kb.queries.GetKeyShareByKeyID(ctx, GetKeyShareByKeyIDParams{
DidID: am.kb.didID,
KeyID: keyID,
})
if err != nil {
return nil, fmt.Errorf("get key share by key ID: %w", err)
}
return keyShareToResult(&ks), nil
}
func (am *ActionManager) RotateKeyShare(ctx context.Context, shareID string) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
ks, err := am.kb.queries.GetKeyShareByID(ctx, shareID)
if err != nil {
return fmt.Errorf("get key share: %w", err)
}
return am.kb.queries.RotateKeyShare(ctx, ks.ID)
}
func (am *ActionManager) ArchiveKeyShare(ctx context.Context, shareID string) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
ks, err := am.kb.queries.GetKeyShareByID(ctx, shareID)
if err != nil {
return fmt.Errorf("get key share: %w", err)
}
return am.kb.queries.ArchiveKeyShare(ctx, ks.ID)
}
func (am *ActionManager) DeleteKeyShare(ctx context.Context, shareID string) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
if am.kb.didID == 0 {
return fmt.Errorf("DID not initialized")
}
ks, err := am.kb.queries.GetKeyShareByID(ctx, shareID)
if err != nil {
return fmt.Errorf("get key share: %w", err)
}
return am.kb.queries.DeleteKeyShare(ctx, DeleteKeyShareParams{
ID: ks.ID,
DidID: am.kb.didID,
})
}
func generateShareID() string {
b := make([]byte, 16)
rand.Read(b)
return "ks_" + hex.EncodeToString(b)
}
func keyShareToResult(ks *KeyShare) *KeyShareResult {
chainCode := ""
if ks.ChainCode != nil {
chainCode = *ks.ChainCode
}
derivationPath := ""
if ks.DerivationPath != nil {
derivationPath = *ks.DerivationPath
}
rotatedAt := ""
if ks.RotatedAt != nil {
rotatedAt = *ks.RotatedAt
}
return &KeyShareResult{
ID: ks.ID,
ShareID: ks.ShareID,
KeyID: ks.KeyID,
PartyIndex: ks.PartyIndex,
Threshold: ks.Threshold,
TotalParties: ks.TotalParties,
Curve: ks.Curve,
PublicKey: ks.PublicKey,
ChainCode: chainCode,
DerivationPath: derivationPath,
Status: ks.Status,
CreatedAt: ks.CreatedAt,
RotatedAt: rotatedAt,
}
}

View File

@@ -0,0 +1,172 @@
package keybase
import (
"context"
"encoding/json"
"fmt"
)
type ServiceResult struct {
ID int64 `json:"id"`
Origin string `json:"origin"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
LogoURL string `json:"logo_url,omitempty"`
DID string `json:"did,omitempty"`
IsVerified bool `json:"is_verified"`
Metadata json.RawMessage `json:"metadata,omitempty"`
CreatedAt string `json:"created_at"`
}
type NewServiceInput struct {
Origin string `json:"origin"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
LogoURL string `json:"logo_url,omitempty"`
DID string `json:"did,omitempty"`
IsVerified bool `json:"is_verified"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
type UpdateServiceInput struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
LogoURL string `json:"logo_url,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
func (am *ActionManager) CreateService(ctx context.Context, params NewServiceInput) (*ServiceResult, error) {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
var description, logoURL, did *string
if params.Description != "" {
description = &params.Description
}
if params.LogoURL != "" {
logoURL = &params.LogoURL
}
if params.DID != "" {
did = &params.DID
}
metadata := params.Metadata
if metadata == nil {
metadata = json.RawMessage(`{}`)
}
var isVerified int64
if params.IsVerified {
isVerified = 1
}
svc, err := am.kb.queries.CreateService(ctx, CreateServiceParams{
Origin: params.Origin,
Name: params.Name,
Description: description,
LogoUrl: logoURL,
Did: did,
IsVerified: isVerified,
Metadata: metadata,
})
if err != nil {
return nil, fmt.Errorf("create service: %w", err)
}
return serviceToResult(&svc), nil
}
func (am *ActionManager) GetServiceByOrigin(ctx context.Context, origin string) (*ServiceResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
svc, err := am.kb.queries.GetServiceByOrigin(ctx, origin)
if err != nil {
return nil, fmt.Errorf("get service by origin: %w", err)
}
return serviceToResult(&svc), nil
}
func (am *ActionManager) GetServiceByID(ctx context.Context, serviceID int64) (*ServiceResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
svc, err := am.kb.queries.GetServiceByID(ctx, serviceID)
if err != nil {
return nil, fmt.Errorf("get service by ID: %w", err)
}
return serviceToResult(&svc), nil
}
func (am *ActionManager) UpdateService(ctx context.Context, params UpdateServiceInput) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
var description, logoURL *string
if params.Description != "" {
description = &params.Description
}
if params.LogoURL != "" {
logoURL = &params.LogoURL
}
metadata := params.Metadata
if metadata == nil {
metadata = json.RawMessage(`{}`)
}
return am.kb.queries.UpdateService(ctx, UpdateServiceParams{
Name: params.Name,
Description: description,
LogoUrl: logoURL,
Metadata: metadata,
ID: params.ID,
})
}
func (am *ActionManager) ListVerifiedServices(ctx context.Context) ([]ServiceResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
services, err := am.kb.queries.ListVerifiedServices(ctx)
if err != nil {
return nil, fmt.Errorf("list verified services: %w", err)
}
results := make([]ServiceResult, len(services))
for i, svc := range services {
results[i] = *serviceToResult(&svc)
}
return results, nil
}
func serviceToResult(svc *Service) *ServiceResult {
description := ""
if svc.Description != nil {
description = *svc.Description
}
logoURL := ""
if svc.LogoUrl != nil {
logoURL = *svc.LogoUrl
}
did := ""
if svc.Did != nil {
did = *svc.Did
}
return &ServiceResult{
ID: svc.ID,
Origin: svc.Origin,
Name: svc.Name,
Description: description,
LogoURL: logoURL,
DID: did,
IsVerified: svc.IsVerified == 1,
Metadata: svc.Metadata,
CreatedAt: svc.CreatedAt,
}
}

View File

@@ -0,0 +1,84 @@
package keybase
import (
"context"
"fmt"
)
func (am *ActionManager) GetSessionByID(ctx context.Context, sessionID string) (*SessionResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
sess, err := am.kb.queries.GetSessionByID(ctx, sessionID)
if err != nil {
return nil, fmt.Errorf("get session: %w", err)
}
return sessionToResult(&sess), nil
}
func (am *ActionManager) GetCurrentSession(ctx context.Context) (*SessionResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
if am.kb.didID == 0 {
return nil, fmt.Errorf("DID not initialized")
}
sess, err := am.kb.queries.GetCurrentSession(ctx, am.kb.didID)
if err != nil {
return nil, fmt.Errorf("get current session: %w", err)
}
return sessionToResult(&sess), nil
}
func (am *ActionManager) UpdateSessionActivity(ctx context.Context, sessionID string) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
sess, err := am.kb.queries.GetSessionByID(ctx, sessionID)
if err != nil {
return fmt.Errorf("get session: %w", err)
}
return am.kb.queries.UpdateSessionActivity(ctx, sess.ID)
}
func (am *ActionManager) SetCurrentSession(ctx context.Context, sessionID string) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
if am.kb.didID == 0 {
return fmt.Errorf("DID not initialized")
}
sess, err := am.kb.queries.GetSessionByID(ctx, sessionID)
if err != nil {
return fmt.Errorf("get session: %w", err)
}
return am.kb.queries.SetCurrentSession(ctx, SetCurrentSessionParams{
ID: sess.ID,
DidID: am.kb.didID,
})
}
func (am *ActionManager) DeleteExpiredSessions(ctx context.Context) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
return am.kb.queries.DeleteExpiredSessions(ctx)
}
func sessionToResult(sess *Session) *SessionResult {
return &SessionResult{
ID: sess.ID,
SessionID: sess.SessionID,
DeviceInfo: sess.DeviceInfo,
IsCurrent: sess.IsCurrent == 1,
LastActivity: sess.LastActivity,
ExpiresAt: sess.ExpiresAt,
CreatedAt: sess.CreatedAt,
}
}

View File

@@ -0,0 +1,205 @@
package keybase
import (
"context"
"encoding/json"
"fmt"
)
type UCANResult struct {
ID int64 `json:"id"`
CID string `json:"cid"`
Issuer string `json:"issuer"`
Audience string `json:"audience"`
Subject string `json:"subject,omitempty"`
Capabilities json.RawMessage `json:"capabilities"`
NotBefore string `json:"not_before,omitempty"`
ExpiresAt string `json:"expires_at"`
IsRevoked bool `json:"is_revoked"`
CreatedAt string `json:"created_at"`
}
type NewUCANInput struct {
CID string `json:"cid"`
Issuer string `json:"issuer"`
Audience string `json:"audience"`
Subject string `json:"subject,omitempty"`
Capabilities json.RawMessage `json:"capabilities"`
ProofChain json.RawMessage `json:"proof_chain,omitempty"`
NotBefore string `json:"not_before,omitempty"`
ExpiresAt string `json:"expires_at"`
Nonce string `json:"nonce,omitempty"`
Facts json.RawMessage `json:"facts,omitempty"`
Signature string `json:"signature"`
RawToken string `json:"raw_token"`
}
func (am *ActionManager) CreateUCAN(ctx context.Context, params NewUCANInput) (*UCANResult, error) {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
if am.kb.didID == 0 {
return nil, fmt.Errorf("DID not initialized")
}
var subject, notBefore, nonce *string
if params.Subject != "" {
subject = &params.Subject
}
if params.NotBefore != "" {
notBefore = &params.NotBefore
}
if params.Nonce != "" {
nonce = &params.Nonce
}
proofChain := params.ProofChain
if proofChain == nil {
proofChain = json.RawMessage(`[]`)
}
facts := params.Facts
if facts == nil {
facts = json.RawMessage(`{}`)
}
ucan, err := am.kb.queries.CreateUCAN(ctx, CreateUCANParams{
DidID: am.kb.didID,
Cid: params.CID,
Issuer: params.Issuer,
Audience: params.Audience,
Subject: subject,
Capabilities: params.Capabilities,
ProofChain: proofChain,
NotBefore: notBefore,
ExpiresAt: params.ExpiresAt,
Nonce: nonce,
Facts: facts,
Signature: params.Signature,
RawToken: params.RawToken,
})
if err != nil {
return nil, fmt.Errorf("create ucan: %w", err)
}
return ucanToResult(&ucan), nil
}
func (am *ActionManager) ListUCANs(ctx context.Context) ([]UCANResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
if am.kb.didID == 0 {
return []UCANResult{}, nil
}
ucans, err := am.kb.queries.ListUCANsByDID(ctx, am.kb.didID)
if err != nil {
return nil, fmt.Errorf("list ucans: %w", err)
}
results := make([]UCANResult, len(ucans))
for i, u := range ucans {
results[i] = *ucanToResult(&u)
}
return results, nil
}
func (am *ActionManager) GetUCANByCID(ctx context.Context, cid string) (*UCANResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
ucan, err := am.kb.queries.GetUCANByCID(ctx, cid)
if err != nil {
return nil, fmt.Errorf("get ucan: %w", err)
}
return ucanToResult(&ucan), nil
}
func (am *ActionManager) ListUCANsByAudience(ctx context.Context, audience string) ([]UCANResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
ucans, err := am.kb.queries.ListUCANsByAudience(ctx, audience)
if err != nil {
return nil, fmt.Errorf("list ucans by audience: %w", err)
}
results := make([]UCANResult, len(ucans))
for i, u := range ucans {
results[i] = *ucanToResult(&u)
}
return results, nil
}
func (am *ActionManager) RevokeUCAN(ctx context.Context, cid string) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
return am.kb.queries.RevokeUCAN(ctx, cid)
}
func (am *ActionManager) IsUCANRevoked(ctx context.Context, cid string) (bool, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
revoked, err := am.kb.queries.IsUCANRevoked(ctx, cid)
if err != nil {
return false, fmt.Errorf("check ucan revocation: %w", err)
}
return revoked == 1, nil
}
func (am *ActionManager) CreateRevocation(ctx context.Context, ucanCID string, revokedBy string, reason string) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
var reasonPtr *string
if reason != "" {
reasonPtr = &reason
}
if err := am.kb.queries.RevokeUCAN(ctx, ucanCID); err != nil {
return fmt.Errorf("revoke ucan token: %w", err)
}
return am.kb.queries.CreateRevocation(ctx, CreateRevocationParams{
UcanCid: ucanCID,
RevokedBy: revokedBy,
Reason: reasonPtr,
})
}
func (am *ActionManager) CleanExpiredUCANs(ctx context.Context) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
return am.kb.queries.CleanExpiredUCANs(ctx)
}
func ucanToResult(u *UcanToken) *UCANResult {
subject := ""
if u.Subject != nil {
subject = *u.Subject
}
notBefore := ""
if u.NotBefore != nil {
notBefore = *u.NotBefore
}
return &UCANResult{
ID: u.ID,
CID: u.Cid,
Issuer: u.Issuer,
Audience: u.Audience,
Subject: subject,
Capabilities: u.Capabilities,
NotBefore: notBefore,
ExpiresAt: u.ExpiresAt,
IsRevoked: u.IsRevoked == 1,
CreatedAt: u.CreatedAt,
}
}

View File

@@ -0,0 +1,114 @@
package keybase
import (
"context"
"fmt"
)
type NewVerificationMethodInput struct {
MethodID string `json:"method_id"`
MethodType string `json:"method_type"`
Controller string `json:"controller"`
PublicKey string `json:"public_key"`
Purpose string `json:"purpose"`
}
func (am *ActionManager) CreateVerificationMethod(ctx context.Context, params NewVerificationMethodInput) (*VerificationMethodResult, error) {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
if am.kb.didID == 0 {
return nil, fmt.Errorf("DID not initialized")
}
vm, err := am.kb.queries.CreateVerificationMethod(ctx, CreateVerificationMethodParams{
DidID: am.kb.didID,
MethodID: params.MethodID,
MethodType: params.MethodType,
Controller: params.Controller,
PublicKey: params.PublicKey,
Purpose: params.Purpose,
})
if err != nil {
return nil, fmt.Errorf("create verification method: %w", err)
}
return &VerificationMethodResult{
ID: vm.MethodID,
Type: vm.MethodType,
Controller: vm.Controller,
PublicKey: vm.PublicKey,
Purpose: vm.Purpose,
}, nil
}
func (am *ActionManager) ListVerificationMethodsFull(ctx context.Context) ([]VerificationMethodResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
if am.kb.didID == 0 {
return []VerificationMethodResult{}, nil
}
vms, err := am.kb.queries.ListVerificationMethods(ctx, am.kb.didID)
if err != nil {
return nil, fmt.Errorf("list verification methods: %w", err)
}
results := make([]VerificationMethodResult, len(vms))
for i, vm := range vms {
results[i] = VerificationMethodResult{
ID: vm.MethodID,
Type: vm.MethodType,
Controller: vm.Controller,
PublicKey: vm.PublicKey,
Purpose: vm.Purpose,
}
}
return results, nil
}
func (am *ActionManager) GetVerificationMethod(ctx context.Context, methodID string) (*VerificationMethodResult, error) {
am.kb.mu.RLock()
defer am.kb.mu.RUnlock()
if am.kb.didID == 0 {
return nil, fmt.Errorf("DID not initialized")
}
vm, err := am.kb.queries.GetVerificationMethod(ctx, GetVerificationMethodParams{
DidID: am.kb.didID,
MethodID: methodID,
})
if err != nil {
return nil, fmt.Errorf("get verification method: %w", err)
}
return &VerificationMethodResult{
ID: vm.MethodID,
Type: vm.MethodType,
Controller: vm.Controller,
PublicKey: vm.PublicKey,
Purpose: vm.Purpose,
}, nil
}
func (am *ActionManager) DeleteVerificationMethod(ctx context.Context, methodID string) error {
am.kb.mu.Lock()
defer am.kb.mu.Unlock()
if am.kb.didID == 0 {
return fmt.Errorf("DID not initialized")
}
vm, err := am.kb.queries.GetVerificationMethod(ctx, GetVerificationMethodParams{
DidID: am.kb.didID,
MethodID: methodID,
})
if err != nil {
return fmt.Errorf("get verification method: %w", err)
}
return am.kb.queries.DeleteVerificationMethod(ctx, vm.ID)
}