123 lines
3.3 KiB
Go
123 lines
3.3 KiB
Go
package tenantstore
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/klarkxy/nekonest/relaycore"
|
|
corestore "github.com/klarkxy/nekonest/relaycore/store"
|
|
)
|
|
|
|
var (
|
|
_ corestore.Store = (*DB)(nil)
|
|
_ relaycore.PrincipalSynchronizer = (*DB)(nil)
|
|
)
|
|
|
|
func credentialDigest(credential relaycore.Credential) (string, error) {
|
|
value := strings.TrimSpace(credential.Value)
|
|
switch credential.Kind {
|
|
case relaycore.CredentialRaw:
|
|
if value == "" {
|
|
return "", fmt.Errorf("credential is required")
|
|
}
|
|
return hashToken(value), nil
|
|
case relaycore.CredentialSHA256:
|
|
value = strings.ToLower(value)
|
|
if len(value) != 64 {
|
|
return "", fmt.Errorf("invalid sha256 credential digest")
|
|
}
|
|
for _, char := range value {
|
|
if !strings.ContainsRune("0123456789abcdef", char) {
|
|
return "", fmt.Errorf("invalid sha256 credential digest")
|
|
}
|
|
}
|
|
return value, nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported credential kind %q", credential.Kind)
|
|
}
|
|
}
|
|
|
|
// SyncDevice installs a control-plane-approved identity without ever requiring
|
|
// the plaintext bearer to leave the client/Relay admission boundary.
|
|
func (db *DB) SyncDevice(id, name, osName string, credential relaycore.Credential) error {
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
return fmt.Errorf("device id is required")
|
|
}
|
|
digest, err := credentialDigest(credential)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
name = "Cloud host"
|
|
}
|
|
osName = strings.TrimSpace(osName)
|
|
if osName == "" {
|
|
osName = "unknown"
|
|
}
|
|
now := time.Now().Unix()
|
|
_, err = db.conn.Exec(
|
|
`INSERT INTO devices (id, name, os, token_hash, created_at, last_seen, active_agents, revoked_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, 0, 0)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
name = excluded.name,
|
|
os = excluded.os,
|
|
token_hash = excluded.token_hash,
|
|
revoked_at = 0`,
|
|
id, name, osName, digest, now, now,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// RevokeDevice invalidates the bearer while retaining ciphertext history and
|
|
// audit-relevant local state until the tenant retention policy purges it.
|
|
func (db *DB) RevokeDevice(id string) error {
|
|
result, err := db.conn.Exec(
|
|
`UPDATE devices SET token_hash = ?, revoked_at = ?, active_agents = 0
|
|
WHERE id = ? AND revoked_at = 0`,
|
|
hashToken("revoked:"+generateToken()), time.Now().Unix(), strings.TrimSpace(id),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
changed, err := result.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if changed == 0 {
|
|
return fmt.Errorf("device not found or already revoked")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SyncPhone mirrors an approved phone credential digest into this tenant's
|
|
// isolated store. Device grants remain separate and are never created here.
|
|
func (db *DB) SyncPhone(id, name string, credential relaycore.Credential) error {
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
return fmt.Errorf("phone id is required")
|
|
}
|
|
digest, err := credentialDigest(credential)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
name = "Cloud phone"
|
|
}
|
|
now := time.Now().Unix()
|
|
_, err = db.conn.Exec(
|
|
`INSERT INTO phone_identities
|
|
(id, name, token_hash, ed25519_public, x25519_public, created_at, last_seen, revoked_at)
|
|
VALUES (?, ?, ?, '', '', ?, ?, 0)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
name = excluded.name,
|
|
token_hash = excluded.token_hash,
|
|
revoked_at = 0`,
|
|
id, name, digest, now, now,
|
|
)
|
|
return err
|
|
}
|