feat: establish NekoNest Cloud control and relay
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user