Port envd from e2b with internalized shared packages and Connect RPC
- Copy envd source from e2b-dev/infra, internalize shared dependencies
into envd/internal/shared/ (keys, filesystem, id, smap, utils)
- Switch from gRPC to Connect RPC for all envd services
- Update module paths to git.omukk.dev/wrenn/{sandbox,sandbox/envd}
- Add proto specs (process, filesystem) with buf-based code generation
- Implement full envd: process exec, filesystem ops, port forwarding,
cgroup management, MMDS integration, and HTTP API
- Update main module dependencies (firecracker SDK, pgx, goose, etc.)
- Remove placeholder .gitkeep files replaced by real implementations
This commit is contained in:
212
envd/internal/api/secure_token.go
Normal file
212
envd/internal/api/secure_token.go
Normal file
@ -0,0 +1,212 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/awnumar/memguard"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTokenNotSet = errors.New("access token not set")
|
||||
ErrTokenEmpty = errors.New("empty token not allowed")
|
||||
)
|
||||
|
||||
// SecureToken wraps memguard for secure token storage.
|
||||
// It uses LockedBuffer which provides memory locking, guard pages,
|
||||
// and secure zeroing on destroy.
|
||||
type SecureToken struct {
|
||||
mu sync.RWMutex
|
||||
buffer *memguard.LockedBuffer
|
||||
}
|
||||
|
||||
// Set securely replaces the token, destroying the old one first.
|
||||
// The old token memory is zeroed before the new token is stored.
|
||||
// The input byte slice is wiped after copying to secure memory.
|
||||
// Returns ErrTokenEmpty if token is empty - use Destroy() to clear the token instead.
|
||||
func (s *SecureToken) Set(token []byte) error {
|
||||
if len(token) == 0 {
|
||||
return ErrTokenEmpty
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Destroy old token first (zeros memory)
|
||||
if s.buffer != nil {
|
||||
s.buffer.Destroy()
|
||||
s.buffer = nil
|
||||
}
|
||||
|
||||
// Create new LockedBuffer from bytes (source slice is wiped by memguard)
|
||||
s.buffer = memguard.NewBufferFromBytes(token)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler to securely parse a JSON string
|
||||
// directly into memguard, wiping the input bytes after copying.
|
||||
//
|
||||
// Access tokens are hex-encoded HMAC-SHA256 hashes (64 chars of [0-9a-f]),
|
||||
// so they never contain JSON escape sequences.
|
||||
func (s *SecureToken) UnmarshalJSON(data []byte) error {
|
||||
// JSON strings are quoted, so minimum valid is `""` (2 bytes).
|
||||
if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' {
|
||||
memguard.WipeBytes(data)
|
||||
|
||||
return errors.New("invalid secure token JSON string")
|
||||
}
|
||||
|
||||
content := data[1 : len(data)-1]
|
||||
|
||||
// Access tokens are hex strings - reject if contains backslash
|
||||
if bytes.ContainsRune(content, '\\') {
|
||||
memguard.WipeBytes(data)
|
||||
|
||||
return errors.New("invalid secure token: unexpected escape sequence")
|
||||
}
|
||||
|
||||
if len(content) == 0 {
|
||||
memguard.WipeBytes(data)
|
||||
|
||||
return ErrTokenEmpty
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.buffer != nil {
|
||||
s.buffer.Destroy()
|
||||
s.buffer = nil
|
||||
}
|
||||
|
||||
// Allocate secure buffer and copy directly into it
|
||||
s.buffer = memguard.NewBuffer(len(content))
|
||||
copy(s.buffer.Bytes(), content)
|
||||
|
||||
// Wipe the input data
|
||||
memguard.WipeBytes(data)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TakeFrom transfers the token from src to this SecureToken, destroying any
|
||||
// existing token. The source token is cleared after transfer.
|
||||
// This avoids copying the underlying bytes.
|
||||
func (s *SecureToken) TakeFrom(src *SecureToken) {
|
||||
if src == nil || s == src {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract buffer from source
|
||||
src.mu.Lock()
|
||||
buffer := src.buffer
|
||||
src.buffer = nil
|
||||
src.mu.Unlock()
|
||||
|
||||
// Install buffer in destination
|
||||
s.mu.Lock()
|
||||
if s.buffer != nil {
|
||||
s.buffer.Destroy()
|
||||
}
|
||||
s.buffer = buffer
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Equals checks if token matches using constant-time comparison.
|
||||
// Returns false if the receiver is nil.
|
||||
func (s *SecureToken) Equals(token string) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.buffer == nil || !s.buffer.IsAlive() {
|
||||
return false
|
||||
}
|
||||
|
||||
return s.buffer.EqualTo([]byte(token))
|
||||
}
|
||||
|
||||
// EqualsSecure compares this token with another SecureToken using constant-time comparison.
|
||||
// Returns false if either receiver or other is nil.
|
||||
func (s *SecureToken) EqualsSecure(other *SecureToken) bool {
|
||||
if s == nil || other == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if s == other {
|
||||
return s.IsSet()
|
||||
}
|
||||
|
||||
// Get a copy of other's bytes (avoids holding two locks simultaneously)
|
||||
otherBytes, err := other.Bytes()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer memguard.WipeBytes(otherBytes)
|
||||
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.buffer == nil || !s.buffer.IsAlive() {
|
||||
return false
|
||||
}
|
||||
|
||||
return s.buffer.EqualTo(otherBytes)
|
||||
}
|
||||
|
||||
// IsSet returns true if a token is stored.
|
||||
// Returns false if the receiver is nil.
|
||||
func (s *SecureToken) IsSet() bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return s.buffer != nil && s.buffer.IsAlive()
|
||||
}
|
||||
|
||||
// Bytes returns a copy of the token bytes (for signature generation).
|
||||
// The caller should zero the returned slice after use.
|
||||
// Returns ErrTokenNotSet if the receiver is nil.
|
||||
func (s *SecureToken) Bytes() ([]byte, error) {
|
||||
if s == nil {
|
||||
return nil, ErrTokenNotSet
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.buffer == nil || !s.buffer.IsAlive() {
|
||||
return nil, ErrTokenNotSet
|
||||
}
|
||||
|
||||
// Return a copy (unavoidable for signature generation)
|
||||
src := s.buffer.Bytes()
|
||||
result := make([]byte, len(src))
|
||||
copy(result, src)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Destroy securely wipes the token from memory.
|
||||
// No-op if the receiver is nil.
|
||||
func (s *SecureToken) Destroy() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.buffer != nil {
|
||||
s.buffer.Destroy()
|
||||
s.buffer = nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user