Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -0,0 +1,180 @@
|
|||
package passwordstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/awnumar/memguard"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/utils"
|
||||
)
|
||||
|
||||
// Service manages password storage in secure RAM
|
||||
type Service struct {
|
||||
logger *zap.Logger
|
||||
mu sync.RWMutex
|
||||
|
||||
// RAM storage (memguard enclaves) - email -> encrypted password
|
||||
memoryStore map[string]*memguard.Enclave
|
||||
}
|
||||
|
||||
// New creates a new password storage service
|
||||
func New(logger *zap.Logger) *Service {
|
||||
// Initialize memguard
|
||||
memguard.CatchInterrupt()
|
||||
|
||||
return &Service{
|
||||
logger: logger,
|
||||
memoryStore: make(map[string]*memguard.Enclave),
|
||||
}
|
||||
}
|
||||
|
||||
// StorePassword stores password in secure RAM (memguard).
|
||||
// SECURITY NOTE: This method accepts a string for API compatibility with JSON inputs.
|
||||
// The string is immediately converted to []byte and the byte slice is zeroed after
|
||||
// creating the secure enclave. For maximum security, use StorePasswordBytes when
|
||||
// you have direct access to []byte data.
|
||||
func (s *Service) StorePassword(email, password string) error {
|
||||
// Convert string to byte slice for secure handling
|
||||
passwordBytes := []byte(password)
|
||||
|
||||
// Store using the secure byte-based method
|
||||
err := s.StorePasswordBytes(email, passwordBytes)
|
||||
|
||||
// Zero the byte slice after use (defense in depth)
|
||||
// Note: The original string cannot be zeroed in Go, but we minimize exposure
|
||||
// by zeroing the byte slice copy as soon as possible
|
||||
zeroBytes(passwordBytes)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// StorePasswordBytes stores password from []byte in secure RAM (memguard).
|
||||
// This is the preferred method when you have direct access to password bytes,
|
||||
// as it allows the caller to zero the source bytes after this call returns.
|
||||
// The provided byte slice is copied into secure memory and can be safely zeroed
|
||||
// by the caller after this method returns.
|
||||
func (s *Service) StorePasswordBytes(email string, password []byte) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Remove existing enclave if any (will be garbage collected)
|
||||
if _, exists := s.memoryStore[email]; exists {
|
||||
delete(s.memoryStore, email)
|
||||
s.logger.Debug("Replaced existing password enclave",
|
||||
zap.String("email", utils.MaskEmail(email)))
|
||||
}
|
||||
|
||||
// Create new secure enclave (memguard copies the data into protected memory)
|
||||
enclave := memguard.NewEnclave(password)
|
||||
s.memoryStore[email] = enclave
|
||||
|
||||
s.logger.Debug("Password stored in secure RAM",
|
||||
zap.String("email", utils.MaskEmail(email)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPassword retrieves password from secure RAM as a string.
|
||||
// SECURITY NOTE: The returned string cannot be zeroed in Go. For operations
|
||||
// that can work with []byte, use GetPasswordBytes instead.
|
||||
func (s *Service) GetPassword(email string) (string, error) {
|
||||
passwordBytes, err := s.GetPasswordBytes(email)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Convert to string (unfortunately creates a copy that can't be zeroed)
|
||||
password := string(passwordBytes)
|
||||
|
||||
// Zero the byte slice
|
||||
zeroBytes(passwordBytes)
|
||||
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// GetPasswordBytes retrieves password from secure RAM as []byte.
|
||||
// The caller SHOULD zero the returned byte slice after use by calling
|
||||
// zeroBytes or similar. This is the preferred method for security-sensitive
|
||||
// operations.
|
||||
func (s *Service) GetPasswordBytes(email string) ([]byte, error) {
|
||||
s.mu.RLock()
|
||||
enclave, exists := s.memoryStore[email]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no password stored for %s", email)
|
||||
}
|
||||
|
||||
// Open enclave to read password
|
||||
lockedBuffer, err := enclave.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open password enclave: %w", err)
|
||||
}
|
||||
defer lockedBuffer.Destroy()
|
||||
|
||||
// Copy the password bytes (memguard buffer will be destroyed after defer)
|
||||
passwordBytes := make([]byte, len(lockedBuffer.Bytes()))
|
||||
copy(passwordBytes, lockedBuffer.Bytes())
|
||||
|
||||
return passwordBytes, nil
|
||||
}
|
||||
|
||||
// HasPassword checks if password is stored for given email
|
||||
func (s *Service) HasPassword(email string) bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
_, exists := s.memoryStore[email]
|
||||
return exists
|
||||
}
|
||||
|
||||
// ClearPassword removes password from RAM (logout)
|
||||
func (s *Service) ClearPassword(email string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.memoryStore[email]; exists {
|
||||
delete(s.memoryStore, email)
|
||||
s.logger.Debug("Password cleared from RAM",
|
||||
zap.String("email", utils.MaskEmail(email)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup destroys all secure memory (called on shutdown)
|
||||
func (s *Service) Cleanup() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Log cleanup for each stored password
|
||||
for email := range s.memoryStore {
|
||||
s.logger.Debug("Clearing password enclave on shutdown",
|
||||
zap.String("email", utils.MaskEmail(email)))
|
||||
}
|
||||
|
||||
// Clear the map (enclaves will be garbage collected)
|
||||
s.memoryStore = make(map[string]*memguard.Enclave)
|
||||
|
||||
// Purge all memguard secure memory
|
||||
memguard.Purge()
|
||||
|
||||
s.logger.Debug("Password store cleanup complete - all secure memory purged")
|
||||
}
|
||||
|
||||
// zeroBytes overwrites a byte slice with zeros to clear sensitive data from memory.
|
||||
// This is a defense-in-depth measure - while Go's GC may still have copies,
|
||||
// this reduces the window of exposure.
|
||||
func zeroBytes(b []byte) {
|
||||
for i := range b {
|
||||
b[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// ZeroBytes is exported for callers who receive password bytes and need to clear them.
|
||||
// Use this after you're done with password bytes returned by GetPasswordBytes.
|
||||
func ZeroBytes(b []byte) {
|
||||
zeroBytes(b)
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package passwordstore
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ProvideService creates the password storage service
|
||||
func ProvideService(logger *zap.Logger) *Service {
|
||||
return New(logger.Named("password-store"))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue