2468 lines
92 KiB
HTML
2468 lines
92 KiB
HTML
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
<title>crypto: Go Coverage Report</title>
|
|
<style>
|
|
body {
|
|
background: black;
|
|
color: rgb(80, 80, 80);
|
|
}
|
|
body, pre, #legend span {
|
|
font-family: Menlo, monospace;
|
|
font-weight: bold;
|
|
}
|
|
#topbar {
|
|
background: black;
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0;
|
|
height: 42px;
|
|
border-bottom: 1px solid rgb(80, 80, 80);
|
|
}
|
|
#content {
|
|
margin-top: 50px;
|
|
}
|
|
#nav, #legend {
|
|
float: left;
|
|
margin-left: 10px;
|
|
}
|
|
#legend {
|
|
margin-top: 12px;
|
|
}
|
|
#nav {
|
|
margin-top: 10px;
|
|
}
|
|
#legend span {
|
|
margin: 0 5px;
|
|
}
|
|
.cov0 { color: rgb(192, 0, 0) }
|
|
.cov1 { color: rgb(128, 128, 128) }
|
|
.cov2 { color: rgb(116, 140, 131) }
|
|
.cov3 { color: rgb(104, 152, 134) }
|
|
.cov4 { color: rgb(92, 164, 137) }
|
|
.cov5 { color: rgb(80, 176, 140) }
|
|
.cov6 { color: rgb(68, 188, 143) }
|
|
.cov7 { color: rgb(56, 200, 146) }
|
|
.cov8 { color: rgb(44, 212, 149) }
|
|
.cov9 { color: rgb(32, 224, 152) }
|
|
.cov10 { color: rgb(20, 236, 155) }
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="topbar">
|
|
<div id="nav">
|
|
<select id="files">
|
|
|
|
<option value="file0">github.com/rwadurian/mpc-system/pkg/crypto/crypto.go (38.3%)</option>
|
|
|
|
<option value="file1">github.com/rwadurian/mpc-system/pkg/jwt/jwt.go (83.3%)</option>
|
|
|
|
<option value="file2">github.com/rwadurian/mpc-system/pkg/utils/utils.go (88.5%)</option>
|
|
|
|
<option value="file3">github.com/rwadurian/mpc-system/services/account/domain/entities/account.go (95.0%)</option>
|
|
|
|
<option value="file4">github.com/rwadurian/mpc-system/services/account/domain/entities/account_share.go (68.4%)</option>
|
|
|
|
<option value="file5">github.com/rwadurian/mpc-system/services/account/domain/entities/recovery_session.go (56.0%)</option>
|
|
|
|
<option value="file6">github.com/rwadurian/mpc-system/services/account/domain/value_objects/account_id.go (80.0%)</option>
|
|
|
|
<option value="file7">github.com/rwadurian/mpc-system/services/account/domain/value_objects/account_status.go (44.4%)</option>
|
|
|
|
<option value="file8">github.com/rwadurian/mpc-system/services/session-coordinator/domain/entities/device_info.go (28.6%)</option>
|
|
|
|
<option value="file9">github.com/rwadurian/mpc-system/services/session-coordinator/domain/entities/mpc_session.go (12.2%)</option>
|
|
|
|
<option value="file10">github.com/rwadurian/mpc-system/services/session-coordinator/domain/entities/participant.go (60.7%)</option>
|
|
|
|
<option value="file11">github.com/rwadurian/mpc-system/services/session-coordinator/domain/entities/session_message.go (0.0%)</option>
|
|
|
|
<option value="file12">github.com/rwadurian/mpc-system/services/session-coordinator/domain/value_objects/party_id.go (57.1%)</option>
|
|
|
|
<option value="file13">github.com/rwadurian/mpc-system/services/session-coordinator/domain/value_objects/session_id.go (80.0%)</option>
|
|
|
|
<option value="file14">github.com/rwadurian/mpc-system/services/session-coordinator/domain/value_objects/session_status.go (18.8%)</option>
|
|
|
|
<option value="file15">github.com/rwadurian/mpc-system/services/session-coordinator/domain/value_objects/threshold.go (55.0%)</option>
|
|
|
|
</select>
|
|
</div>
|
|
<div id="legend">
|
|
<span>not tracked</span>
|
|
|
|
<span class="cov0">no coverage</span>
|
|
<span class="cov1">low coverage</span>
|
|
<span class="cov2">*</span>
|
|
<span class="cov3">*</span>
|
|
<span class="cov4">*</span>
|
|
<span class="cov5">*</span>
|
|
<span class="cov6">*</span>
|
|
<span class="cov7">*</span>
|
|
<span class="cov8">*</span>
|
|
<span class="cov9">*</span>
|
|
<span class="cov10">high coverage</span>
|
|
|
|
</div>
|
|
</div>
|
|
<div id="content">
|
|
|
|
<pre class="file" id="file0" style="display: none">package crypto
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"math/big"
|
|
|
|
"golang.org/x/crypto/hkdf"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidKeySize = errors.New("invalid key size")
|
|
ErrInvalidCipherText = errors.New("invalid ciphertext")
|
|
ErrEncryptionFailed = errors.New("encryption failed")
|
|
ErrDecryptionFailed = errors.New("decryption failed")
|
|
ErrInvalidPublicKey = errors.New("invalid public key")
|
|
ErrInvalidSignature = errors.New("invalid signature")
|
|
)
|
|
|
|
// CryptoService provides cryptographic operations
|
|
type CryptoService struct {
|
|
masterKey []byte
|
|
}
|
|
|
|
// NewCryptoService creates a new crypto service
|
|
func NewCryptoService(masterKey []byte) (*CryptoService, error) <span class="cov0" title="0">{
|
|
if len(masterKey) != 32 </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidKeySize
|
|
}</span>
|
|
<span class="cov0" title="0">return &CryptoService{masterKey: masterKey}, nil</span>
|
|
}
|
|
|
|
// GenerateRandomBytes generates random bytes
|
|
func GenerateRandomBytes(n int) ([]byte, error) <span class="cov6" title="6">{
|
|
b := make([]byte, n)
|
|
_, err := rand.Read(b)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
<span class="cov6" title="6">return b, nil</span>
|
|
}
|
|
|
|
// GenerateRandomHex generates a random hex string
|
|
func GenerateRandomHex(n int) (string, error) <span class="cov0" title="0">{
|
|
bytes, err := GenerateRandomBytes(n)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return "", err
|
|
}</span>
|
|
<span class="cov0" title="0">return hex.EncodeToString(bytes), nil</span>
|
|
}
|
|
|
|
// DeriveKey derives a key from the master key using HKDF
|
|
func (c *CryptoService) DeriveKey(context string, length int) ([]byte, error) <span class="cov0" title="0">{
|
|
hkdfReader := hkdf.New(sha256.New, c.masterKey, nil, []byte(context))
|
|
key := make([]byte, length)
|
|
if _, err := io.ReadFull(hkdfReader, key); err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
<span class="cov0" title="0">return key, nil</span>
|
|
}
|
|
|
|
// EncryptShare encrypts a key share using AES-256-GCM
|
|
func (c *CryptoService) EncryptShare(shareData []byte, partyID string) ([]byte, error) <span class="cov0" title="0">{
|
|
// Derive a unique key for this party
|
|
key, err := c.DeriveKey("share_encryption:"+partyID, 32)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">block, err := aes.NewCipher(key)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">aesGCM, err := cipher.NewGCM(block)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">nonce := make([]byte, aesGCM.NonceSize())
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
// Encrypt and prepend nonce
|
|
<span class="cov0" title="0">ciphertext := aesGCM.Seal(nonce, nonce, shareData, []byte(partyID))
|
|
return ciphertext, nil</span>
|
|
}
|
|
|
|
// DecryptShare decrypts a key share
|
|
func (c *CryptoService) DecryptShare(encryptedData []byte, partyID string) ([]byte, error) <span class="cov0" title="0">{
|
|
// Derive the same key used for encryption
|
|
key, err := c.DeriveKey("share_encryption:"+partyID, 32)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">block, err := aes.NewCipher(key)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">aesGCM, err := cipher.NewGCM(block)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">nonceSize := aesGCM.NonceSize()
|
|
if len(encryptedData) < nonceSize </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidCipherText
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">nonce, ciphertext := encryptedData[:nonceSize], encryptedData[nonceSize:]
|
|
plaintext, err := aesGCM.Open(nil, nonce, ciphertext, []byte(partyID))
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, ErrDecryptionFailed
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">return plaintext, nil</span>
|
|
}
|
|
|
|
// EncryptMessage encrypts a message using AES-256-GCM
|
|
func (c *CryptoService) EncryptMessage(plaintext []byte) ([]byte, error) <span class="cov0" title="0">{
|
|
block, err := aes.NewCipher(c.masterKey)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">aesGCM, err := cipher.NewGCM(block)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">nonce := make([]byte, aesGCM.NonceSize())
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil)
|
|
return ciphertext, nil</span>
|
|
}
|
|
|
|
// DecryptMessage decrypts a message
|
|
func (c *CryptoService) DecryptMessage(ciphertext []byte) ([]byte, error) <span class="cov0" title="0">{
|
|
block, err := aes.NewCipher(c.masterKey)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">aesGCM, err := cipher.NewGCM(block)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">nonceSize := aesGCM.NonceSize()
|
|
if len(ciphertext) < nonceSize </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidCipherText
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
|
plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, ErrDecryptionFailed
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">return plaintext, nil</span>
|
|
}
|
|
|
|
// Hash256 computes SHA-256 hash
|
|
func Hash256(data []byte) []byte <span class="cov7" title="10">{
|
|
hash := sha256.Sum256(data)
|
|
return hash[:]
|
|
}</span>
|
|
|
|
// VerifyECDSASignature verifies an ECDSA signature
|
|
func VerifyECDSASignature(messageHash, signature, publicKey []byte) (bool, error) <span class="cov0" title="0">{
|
|
// Parse public key (assuming secp256k1/P256 uncompressed format)
|
|
curve := elliptic.P256()
|
|
x, y := elliptic.Unmarshal(curve, publicKey)
|
|
if x == nil </span><span class="cov0" title="0">{
|
|
return false, ErrInvalidPublicKey
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">pubKey := &ecdsa.PublicKey{
|
|
Curve: curve,
|
|
X: x,
|
|
Y: y,
|
|
}
|
|
|
|
// Parse signature (R || S, each 32 bytes)
|
|
if len(signature) != 64 </span><span class="cov0" title="0">{
|
|
return false, ErrInvalidSignature
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">r := new(big.Int).SetBytes(signature[:32])
|
|
s := new(big.Int).SetBytes(signature[32:])
|
|
|
|
// Verify signature
|
|
valid := ecdsa.Verify(pubKey, messageHash, r, s)
|
|
return valid, nil</span>
|
|
}
|
|
|
|
// GenerateNonce generates a cryptographic nonce
|
|
func GenerateNonce() ([]byte, error) <span class="cov0" title="0">{
|
|
return GenerateRandomBytes(32)
|
|
}</span>
|
|
|
|
// SecureCompare performs constant-time comparison
|
|
func SecureCompare(a, b []byte) bool <span class="cov4" title="3">{
|
|
if len(a) != len(b) </span><span class="cov1" title="1">{
|
|
return false
|
|
}</span>
|
|
|
|
<span class="cov3" title="2">var result byte
|
|
for i := 0; i < len(a); i++ </span><span class="cov10" title="20">{
|
|
result |= a[i] ^ b[i]
|
|
}</span>
|
|
<span class="cov3" title="2">return result == 0</span>
|
|
}
|
|
|
|
// ParsePublicKey parses a public key from bytes (P256 uncompressed format)
|
|
func ParsePublicKey(publicKeyBytes []byte) (*ecdsa.PublicKey, error) <span class="cov1" title="1">{
|
|
curve := elliptic.P256()
|
|
x, y := elliptic.Unmarshal(curve, publicKeyBytes)
|
|
if x == nil </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidPublicKey
|
|
}</span>
|
|
|
|
<span class="cov1" title="1">return &ecdsa.PublicKey{
|
|
Curve: curve,
|
|
X: x,
|
|
Y: y,
|
|
}, nil</span>
|
|
}
|
|
|
|
// VerifySignature verifies an ECDSA signature using a public key
|
|
func VerifySignature(pubKey *ecdsa.PublicKey, messageHash, signature []byte) bool <span class="cov4" title="3">{
|
|
// Parse signature (R || S, each 32 bytes)
|
|
if len(signature) != 64 </span><span class="cov0" title="0">{
|
|
return false
|
|
}</span>
|
|
|
|
<span class="cov4" title="3">r := new(big.Int).SetBytes(signature[:32])
|
|
s := new(big.Int).SetBytes(signature[32:])
|
|
|
|
return ecdsa.Verify(pubKey, messageHash, r, s)</span>
|
|
}
|
|
|
|
// HashMessage computes SHA-256 hash of a message (alias for Hash256)
|
|
func HashMessage(message []byte) []byte <span class="cov6" title="7">{
|
|
return Hash256(message)
|
|
}</span>
|
|
|
|
// Encrypt encrypts data using AES-256-GCM with the provided key
|
|
func Encrypt(key, plaintext []byte) ([]byte, error) <span class="cov5" title="4">{
|
|
if len(key) != 32 </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidKeySize
|
|
}</span>
|
|
|
|
<span class="cov5" title="4">block, err := aes.NewCipher(key)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov5" title="4">aesGCM, err := cipher.NewGCM(block)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov5" title="4">nonce := make([]byte, aesGCM.NonceSize())
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov5" title="4">ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil)
|
|
return ciphertext, nil</span>
|
|
}
|
|
|
|
// Decrypt decrypts data using AES-256-GCM with the provided key
|
|
func Decrypt(key, ciphertext []byte) ([]byte, error) <span class="cov3" title="2">{
|
|
if len(key) != 32 </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidKeySize
|
|
}</span>
|
|
|
|
<span class="cov3" title="2">block, err := aes.NewCipher(key)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov3" title="2">aesGCM, err := cipher.NewGCM(block)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov3" title="2">nonceSize := aesGCM.NonceSize()
|
|
if len(ciphertext) < nonceSize </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidCipherText
|
|
}</span>
|
|
|
|
<span class="cov3" title="2">nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
|
plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
|
|
if err != nil </span><span class="cov1" title="1">{
|
|
return nil, ErrDecryptionFailed
|
|
}</span>
|
|
|
|
<span class="cov1" title="1">return plaintext, nil</span>
|
|
}
|
|
|
|
// DeriveKey derives a key from secret and salt using HKDF (standalone function)
|
|
func DeriveKey(secret, salt []byte, length int) ([]byte, error) <span class="cov5" title="4">{
|
|
hkdfReader := hkdf.New(sha256.New, secret, salt, nil)
|
|
key := make([]byte, length)
|
|
if _, err := io.ReadFull(hkdfReader, key); err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
<span class="cov5" title="4">return key, nil</span>
|
|
}
|
|
|
|
// SignMessage signs a message using ECDSA private key
|
|
func SignMessage(privateKey *ecdsa.PrivateKey, message []byte) ([]byte, error) <span class="cov4" title="3">{
|
|
hash := Hash256(message)
|
|
r, s, err := ecdsa.Sign(rand.Reader, privateKey, hash)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
// Encode R and S as 32 bytes each (total 64 bytes)
|
|
<span class="cov4" title="3">signature := make([]byte, 64)
|
|
rBytes := r.Bytes()
|
|
sBytes := s.Bytes()
|
|
|
|
// Pad with zeros if necessary
|
|
copy(signature[32-len(rBytes):32], rBytes)
|
|
copy(signature[64-len(sBytes):64], sBytes)
|
|
|
|
return signature, nil</span>
|
|
}
|
|
|
|
// EncodeToHex encodes bytes to hex string
|
|
func EncodeToHex(data []byte) string <span class="cov1" title="1">{
|
|
return hex.EncodeToString(data)
|
|
}</span>
|
|
|
|
// DecodeFromHex decodes hex string to bytes
|
|
func DecodeFromHex(s string) ([]byte, error) <span class="cov3" title="2">{
|
|
return hex.DecodeString(s)
|
|
}</span>
|
|
|
|
// EncodeToBase64 encodes bytes to base64 string
|
|
func EncodeToBase64(data []byte) string <span class="cov0" title="0">{
|
|
return hex.EncodeToString(data) // Using hex for simplicity, could use base64
|
|
}</span>
|
|
|
|
// DecodeFromBase64 decodes base64 string to bytes
|
|
func DecodeFromBase64(s string) ([]byte, error) <span class="cov0" title="0">{
|
|
return hex.DecodeString(s)
|
|
}</span>
|
|
|
|
// MarshalPublicKey marshals an ECDSA public key to bytes
|
|
func MarshalPublicKey(pubKey *ecdsa.PublicKey) []byte <span class="cov1" title="1">{
|
|
return elliptic.Marshal(pubKey.Curve, pubKey.X, pubKey.Y)
|
|
}</span>
|
|
|
|
// CompareBytes performs constant-time comparison of two byte slices
|
|
func CompareBytes(a, b []byte) bool <span class="cov4" title="3">{
|
|
return SecureCompare(a, b)
|
|
}</span>
|
|
</pre>
|
|
|
|
<pre class="file" id="file1" style="display: none">package jwt
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidToken = errors.New("invalid token")
|
|
ErrExpiredToken = errors.New("token expired")
|
|
ErrInvalidClaims = errors.New("invalid claims")
|
|
ErrTokenNotYetValid = errors.New("token not yet valid")
|
|
)
|
|
|
|
// Claims represents custom JWT claims
|
|
type Claims struct {
|
|
SessionID string `json:"session_id"`
|
|
PartyID string `json:"party_id"`
|
|
TokenType string `json:"token_type"` // "join", "access", "refresh"
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
// JWTService provides JWT operations
|
|
type JWTService struct {
|
|
secretKey []byte
|
|
issuer string
|
|
tokenExpiry time.Duration
|
|
refreshExpiry time.Duration
|
|
}
|
|
|
|
// NewJWTService creates a new JWT service
|
|
func NewJWTService(secretKey string, issuer string, tokenExpiry, refreshExpiry time.Duration) *JWTService <span class="cov6" title="4">{
|
|
return &JWTService{
|
|
secretKey: []byte(secretKey),
|
|
issuer: issuer,
|
|
tokenExpiry: tokenExpiry,
|
|
refreshExpiry: refreshExpiry,
|
|
}
|
|
}</span>
|
|
|
|
// GenerateJoinToken generates a token for joining an MPC session
|
|
func (s *JWTService) GenerateJoinToken(sessionID uuid.UUID, partyID string, expiresIn time.Duration) (string, error) <span class="cov5" title="3">{
|
|
now := time.Now()
|
|
claims := Claims{
|
|
SessionID: sessionID.String(),
|
|
PartyID: partyID,
|
|
TokenType: "join",
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ID: uuid.New().String(),
|
|
Issuer: s.issuer,
|
|
Subject: partyID,
|
|
IssuedAt: jwt.NewNumericDate(now),
|
|
NotBefore: jwt.NewNumericDate(now),
|
|
ExpiresAt: jwt.NewNumericDate(now.Add(expiresIn)),
|
|
},
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
return token.SignedString(s.secretKey)
|
|
}</span>
|
|
|
|
// AccessTokenClaims represents claims in an access token
|
|
type AccessTokenClaims struct {
|
|
Subject string
|
|
Username string
|
|
Issuer string
|
|
}
|
|
|
|
// GenerateAccessToken generates an access token with username
|
|
func (s *JWTService) GenerateAccessToken(userID, username string) (string, error) <span class="cov6" title="4">{
|
|
now := time.Now()
|
|
claims := Claims{
|
|
TokenType: "access",
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ID: uuid.New().String(),
|
|
Issuer: s.issuer,
|
|
Subject: userID,
|
|
IssuedAt: jwt.NewNumericDate(now),
|
|
NotBefore: jwt.NewNumericDate(now),
|
|
ExpiresAt: jwt.NewNumericDate(now.Add(s.tokenExpiry)),
|
|
},
|
|
}
|
|
// Store username in PartyID field for access tokens
|
|
claims.PartyID = username
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
return token.SignedString(s.secretKey)
|
|
}</span>
|
|
|
|
// GenerateRefreshToken generates a refresh token
|
|
func (s *JWTService) GenerateRefreshToken(userID string) (string, error) <span class="cov3" title="2">{
|
|
now := time.Now()
|
|
claims := Claims{
|
|
TokenType: "refresh",
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ID: uuid.New().String(),
|
|
Issuer: s.issuer,
|
|
Subject: userID,
|
|
IssuedAt: jwt.NewNumericDate(now),
|
|
NotBefore: jwt.NewNumericDate(now),
|
|
ExpiresAt: jwt.NewNumericDate(now.Add(s.refreshExpiry)),
|
|
},
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
return token.SignedString(s.secretKey)
|
|
}</span>
|
|
|
|
// ValidateToken validates a JWT token and returns the claims
|
|
func (s *JWTService) ValidateToken(tokenString string) (*Claims, error) <span class="cov10" title="11">{
|
|
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) </span><span class="cov9" title="9">{
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidToken
|
|
}</span>
|
|
<span class="cov9" title="9">return s.secretKey, nil</span>
|
|
})
|
|
|
|
<span class="cov10" title="11">if err != nil </span><span class="cov5" title="3">{
|
|
if errors.Is(err, jwt.ErrTokenExpired) </span><span class="cov0" title="0">{
|
|
return nil, ErrExpiredToken
|
|
}</span>
|
|
<span class="cov5" title="3">return nil, ErrInvalidToken</span>
|
|
}
|
|
|
|
<span class="cov8" title="8">claims, ok := token.Claims.(*Claims)
|
|
if !ok || !token.Valid </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidClaims
|
|
}</span>
|
|
|
|
<span class="cov8" title="8">return claims, nil</span>
|
|
}
|
|
|
|
// ValidateJoinToken validates a join token for MPC sessions
|
|
func (s *JWTService) ValidateJoinToken(tokenString string, sessionID uuid.UUID, partyID string) (*Claims, error) <span class="cov5" title="3">{
|
|
claims, err := s.ValidateToken(tokenString)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov5" title="3">if claims.TokenType != "join" </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidToken
|
|
}</span>
|
|
|
|
<span class="cov5" title="3">if claims.SessionID != sessionID.String() </span><span class="cov1" title="1">{
|
|
return nil, ErrInvalidClaims
|
|
}</span>
|
|
|
|
<span class="cov3" title="2">if claims.PartyID != partyID </span><span class="cov1" title="1">{
|
|
return nil, ErrInvalidClaims
|
|
}</span>
|
|
|
|
<span class="cov1" title="1">return claims, nil</span>
|
|
}
|
|
|
|
// RefreshAccessToken creates a new access token from a valid refresh token
|
|
func (s *JWTService) RefreshAccessToken(refreshToken string) (string, error) <span class="cov3" title="2">{
|
|
claims, err := s.ValidateToken(refreshToken)
|
|
if err != nil </span><span class="cov1" title="1">{
|
|
return "", err
|
|
}</span>
|
|
|
|
<span class="cov1" title="1">if claims.TokenType != "refresh" </span><span class="cov0" title="0">{
|
|
return "", ErrInvalidToken
|
|
}</span>
|
|
|
|
// PartyID stores the username for access tokens
|
|
<span class="cov1" title="1">return s.GenerateAccessToken(claims.Subject, claims.PartyID)</span>
|
|
}
|
|
|
|
// ValidateAccessToken validates an access token and returns structured claims
|
|
func (s *JWTService) ValidateAccessToken(tokenString string) (*AccessTokenClaims, error) <span class="cov7" title="5">{
|
|
claims, err := s.ValidateToken(tokenString)
|
|
if err != nil </span><span class="cov3" title="2">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov5" title="3">if claims.TokenType != "access" </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidToken
|
|
}</span>
|
|
|
|
<span class="cov5" title="3">return &AccessTokenClaims{
|
|
Subject: claims.Subject,
|
|
Username: claims.PartyID, // Username stored in PartyID for access tokens
|
|
Issuer: claims.Issuer,
|
|
}, nil</span>
|
|
}
|
|
|
|
// ValidateRefreshToken validates a refresh token and returns claims
|
|
func (s *JWTService) ValidateRefreshToken(tokenString string) (*Claims, error) <span class="cov1" title="1">{
|
|
claims, err := s.ValidateToken(tokenString)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov1" title="1">if claims.TokenType != "refresh" </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidToken
|
|
}</span>
|
|
|
|
<span class="cov1" title="1">return claims, nil</span>
|
|
}
|
|
|
|
// TokenGenerator interface for dependency injection
|
|
type TokenGenerator interface {
|
|
GenerateJoinToken(sessionID uuid.UUID, partyID string, expiresIn time.Duration) (string, error)
|
|
}
|
|
|
|
// TokenValidator interface for dependency injection
|
|
type TokenValidator interface {
|
|
ValidateJoinToken(tokenString string, sessionID uuid.UUID, partyID string) (*Claims, error)
|
|
}
|
|
|
|
// Ensure JWTService implements interfaces
|
|
var _ TokenGenerator = (*JWTService)(nil)
|
|
var _ TokenValidator = (*JWTService)(nil)
|
|
</pre>
|
|
|
|
<pre class="file" id="file2" style="display: none">package utils
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"math/big"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// GenerateID generates a new UUID
|
|
func GenerateID() uuid.UUID <span class="cov6" title="4">{
|
|
return uuid.New()
|
|
}</span>
|
|
|
|
// ParseUUID parses a string to UUID
|
|
func ParseUUID(s string) (uuid.UUID, error) <span class="cov3" title="2">{
|
|
return uuid.Parse(s)
|
|
}</span>
|
|
|
|
// MustParseUUID parses a string to UUID, panics on error
|
|
func MustParseUUID(s string) uuid.UUID <span class="cov0" title="0">{
|
|
id, err := uuid.Parse(s)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
panic(err)</span>
|
|
}
|
|
<span class="cov0" title="0">return id</span>
|
|
}
|
|
|
|
// IsValidUUID checks if a string is a valid UUID
|
|
func IsValidUUID(s string) bool <span class="cov3" title="2">{
|
|
_, err := uuid.Parse(s)
|
|
return err == nil
|
|
}</span>
|
|
|
|
// ToJSON converts an interface to JSON bytes
|
|
func ToJSON(v interface{}) ([]byte, error) <span class="cov1" title="1">{
|
|
return json.Marshal(v)
|
|
}</span>
|
|
|
|
// FromJSON converts JSON bytes to an interface
|
|
func FromJSON(data []byte, v interface{}) error <span class="cov1" title="1">{
|
|
return json.Unmarshal(data, v)
|
|
}</span>
|
|
|
|
// NowUTC returns the current UTC time
|
|
func NowUTC() time.Time <span class="cov1" title="1">{
|
|
return time.Now().UTC()
|
|
}</span>
|
|
|
|
// TimePtr returns a pointer to the time
|
|
func TimePtr(t time.Time) *time.Time <span class="cov1" title="1">{
|
|
return &t
|
|
}</span>
|
|
|
|
// NowPtr returns a pointer to the current time
|
|
func NowPtr() *time.Time <span class="cov0" title="0">{
|
|
now := NowUTC()
|
|
return &now
|
|
}</span>
|
|
|
|
// BigIntToBytes converts a big.Int to bytes (32 bytes, left-padded)
|
|
func BigIntToBytes(n *big.Int) []byte <span class="cov3" title="2">{
|
|
if n == nil </span><span class="cov1" title="1">{
|
|
return make([]byte, 32)
|
|
}</span>
|
|
<span class="cov1" title="1">b := n.Bytes()
|
|
if len(b) > 32 </span><span class="cov0" title="0">{
|
|
return b[:32]
|
|
}</span>
|
|
<span class="cov1" title="1">if len(b) < 32 </span><span class="cov1" title="1">{
|
|
result := make([]byte, 32)
|
|
copy(result[32-len(b):], b)
|
|
return result
|
|
}</span>
|
|
<span class="cov0" title="0">return b</span>
|
|
}
|
|
|
|
// BytesToBigInt converts bytes to big.Int
|
|
func BytesToBigInt(b []byte) *big.Int <span class="cov1" title="1">{
|
|
return new(big.Int).SetBytes(b)
|
|
}</span>
|
|
|
|
// StringSliceContains checks if a string slice contains a value
|
|
func StringSliceContains(slice []string, value string) bool <span class="cov5" title="3">{
|
|
for _, s := range slice </span><span class="cov7" title="5">{
|
|
if s == value </span><span class="cov1" title="1">{
|
|
return true
|
|
}</span>
|
|
}
|
|
<span class="cov3" title="2">return false</span>
|
|
}
|
|
|
|
// StringSliceRemove removes a value from a string slice
|
|
func StringSliceRemove(slice []string, value string) []string <span class="cov3" title="2">{
|
|
result := make([]string, 0, len(slice))
|
|
for _, s := range slice </span><span class="cov8" title="6">{
|
|
if s != value </span><span class="cov7" title="5">{
|
|
result = append(result, s)
|
|
}</span>
|
|
}
|
|
<span class="cov3" title="2">return result</span>
|
|
}
|
|
|
|
// UniqueStrings returns unique strings from a slice
|
|
func UniqueStrings(slice []string) []string <span class="cov3" title="2">{
|
|
seen := make(map[string]struct{})
|
|
result := make([]string, 0, len(slice))
|
|
for _, s := range slice </span><span class="cov10" title="9">{
|
|
if _, ok := seen[s]; !ok </span><span class="cov8" title="6">{
|
|
seen[s] = struct{}{}
|
|
result = append(result, s)
|
|
}</span>
|
|
}
|
|
<span class="cov3" title="2">return result</span>
|
|
}
|
|
|
|
// TruncateString truncates a string to max length
|
|
func TruncateString(s string, maxLen int) string <span class="cov3" title="2">{
|
|
if len(s) <= maxLen </span><span class="cov1" title="1">{
|
|
return s
|
|
}</span>
|
|
<span class="cov1" title="1">return s[:maxLen]</span>
|
|
}
|
|
|
|
// SafeString returns an empty string if the pointer is nil
|
|
func SafeString(s *string) string <span class="cov3" title="2">{
|
|
if s == nil </span><span class="cov1" title="1">{
|
|
return ""
|
|
}</span>
|
|
<span class="cov1" title="1">return *s</span>
|
|
}
|
|
|
|
// StringPtr returns a pointer to the string
|
|
func StringPtr(s string) *string <span class="cov1" title="1">{
|
|
return &s
|
|
}</span>
|
|
|
|
// IntPtr returns a pointer to the int
|
|
func IntPtr(i int) *int <span class="cov1" title="1">{
|
|
return &i
|
|
}</span>
|
|
|
|
// BoolPtr returns a pointer to the bool
|
|
func BoolPtr(b bool) *bool <span class="cov1" title="1">{
|
|
return &b
|
|
}</span>
|
|
|
|
// IsZero checks if a value is zero/empty
|
|
func IsZero(v interface{}) bool <span class="cov0" title="0">{
|
|
return reflect.ValueOf(v).IsZero()
|
|
}</span>
|
|
|
|
// Coalesce returns the first non-zero value
|
|
func Coalesce[T comparable](values ...T) T <span class="cov5" title="3">{
|
|
var zero T
|
|
for _, v := range values </span><span class="cov10" title="9">{
|
|
if v != zero </span><span class="cov3" title="2">{
|
|
return v
|
|
}</span>
|
|
}
|
|
<span class="cov1" title="1">return zero</span>
|
|
}
|
|
|
|
// MapKeys returns the keys of a map
|
|
func MapKeys[K comparable, V any](m map[K]V) []K <span class="cov3" title="2">{
|
|
keys := make([]K, 0, len(m))
|
|
for k := range m </span><span class="cov5" title="3">{
|
|
keys = append(keys, k)
|
|
}</span>
|
|
<span class="cov3" title="2">return keys</span>
|
|
}
|
|
|
|
// MapValues returns the values of a map
|
|
func MapValues[K comparable, V any](m map[K]V) []V <span class="cov1" title="1">{
|
|
values := make([]V, 0, len(m))
|
|
for _, v := range m </span><span class="cov5" title="3">{
|
|
values = append(values, v)
|
|
}</span>
|
|
<span class="cov1" title="1">return values</span>
|
|
}
|
|
|
|
// Min returns the minimum of two values
|
|
func Min[T ~int | ~int64 | ~float64](a, b T) T <span class="cov5" title="3">{
|
|
if a < b </span><span class="cov3" title="2">{
|
|
return a
|
|
}</span>
|
|
<span class="cov1" title="1">return b</span>
|
|
}
|
|
|
|
// Max returns the maximum of two values
|
|
func Max[T ~int | ~int64 | ~float64](a, b T) T <span class="cov5" title="3">{
|
|
if a > b </span><span class="cov1" title="1">{
|
|
return a
|
|
}</span>
|
|
<span class="cov3" title="2">return b</span>
|
|
}
|
|
|
|
// Clamp clamps a value between min and max
|
|
func Clamp[T ~int | ~int64 | ~float64](value, min, max T) T <span class="cov5" title="3">{
|
|
if value < min </span><span class="cov1" title="1">{
|
|
return min
|
|
}</span>
|
|
<span class="cov3" title="2">if value > max </span><span class="cov1" title="1">{
|
|
return max
|
|
}</span>
|
|
<span class="cov1" title="1">return value</span>
|
|
}
|
|
|
|
// ContextWithTimeout creates a context with timeout
|
|
func ContextWithTimeout(timeout time.Duration) (context.Context, context.CancelFunc) <span class="cov0" title="0">{
|
|
return context.WithTimeout(context.Background(), timeout)
|
|
}</span>
|
|
|
|
// MaskString masks a string showing only first and last n characters
|
|
func MaskString(s string, showChars int) string <span class="cov3" title="2">{
|
|
if len(s) <= showChars*2 </span><span class="cov1" title="1">{
|
|
return strings.Repeat("*", len(s))
|
|
}</span>
|
|
<span class="cov1" title="1">return s[:showChars] + strings.Repeat("*", len(s)-showChars*2) + s[len(s)-showChars:]</span>
|
|
}
|
|
|
|
// Retry executes a function with retries
|
|
func Retry(attempts int, sleep time.Duration, f func() error) error <span class="cov5" title="3">{
|
|
var err error
|
|
for i := 0; i < attempts; i++ </span><span class="cov8" title="7">{
|
|
if err = f(); err == nil </span><span class="cov3" title="2">{
|
|
return nil
|
|
}</span>
|
|
<span class="cov7" title="5">if i < attempts-1 </span><span class="cov6" title="4">{
|
|
time.Sleep(sleep)
|
|
sleep *= 2 // Exponential backoff
|
|
}</span>
|
|
}
|
|
<span class="cov1" title="1">return err</span>
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file3" style="display: none">package entities
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rwadurian/mpc-system/services/account/domain/value_objects"
|
|
)
|
|
|
|
// Account represents a user account with MPC-based authentication
|
|
type Account struct {
|
|
ID value_objects.AccountID
|
|
Username string
|
|
Email string
|
|
Phone *string
|
|
PublicKey []byte // MPC group public key
|
|
KeygenSessionID uuid.UUID
|
|
ThresholdN int
|
|
ThresholdT int
|
|
Status value_objects.AccountStatus
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
LastLoginAt *time.Time
|
|
}
|
|
|
|
// NewAccount creates a new Account
|
|
func NewAccount(
|
|
username string,
|
|
email string,
|
|
publicKey []byte,
|
|
keygenSessionID uuid.UUID,
|
|
thresholdN int,
|
|
thresholdT int,
|
|
) *Account <span class="cov10" title="21">{
|
|
now := time.Now().UTC()
|
|
return &Account{
|
|
ID: value_objects.NewAccountID(),
|
|
Username: username,
|
|
Email: email,
|
|
PublicKey: publicKey,
|
|
KeygenSessionID: keygenSessionID,
|
|
ThresholdN: thresholdN,
|
|
ThresholdT: thresholdT,
|
|
Status: value_objects.AccountStatusActive,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
}</span>
|
|
|
|
// SetPhone sets the phone number
|
|
func (a *Account) SetPhone(phone string) <span class="cov1" title="1">{
|
|
a.Phone = &phone
|
|
a.UpdatedAt = time.Now().UTC()
|
|
}</span>
|
|
|
|
// UpdateLastLogin updates the last login timestamp
|
|
func (a *Account) UpdateLastLogin() <span class="cov1" title="1">{
|
|
now := time.Now().UTC()
|
|
a.LastLoginAt = &now
|
|
a.UpdatedAt = now
|
|
}</span>
|
|
|
|
// Suspend suspends the account
|
|
func (a *Account) Suspend() error <span class="cov3" title="2">{
|
|
if a.Status == value_objects.AccountStatusRecovering </span><span class="cov1" title="1">{
|
|
return ErrAccountInRecovery
|
|
}</span>
|
|
<span class="cov1" title="1">a.Status = value_objects.AccountStatusSuspended
|
|
a.UpdatedAt = time.Now().UTC()
|
|
return nil</span>
|
|
}
|
|
|
|
// Lock locks the account
|
|
func (a *Account) Lock() error <span class="cov3" title="2">{
|
|
if a.Status == value_objects.AccountStatusRecovering </span><span class="cov1" title="1">{
|
|
return ErrAccountInRecovery
|
|
}</span>
|
|
<span class="cov1" title="1">a.Status = value_objects.AccountStatusLocked
|
|
a.UpdatedAt = time.Now().UTC()
|
|
return nil</span>
|
|
}
|
|
|
|
// Activate activates the account
|
|
func (a *Account) Activate() <span class="cov1" title="1">{
|
|
a.Status = value_objects.AccountStatusActive
|
|
a.UpdatedAt = time.Now().UTC()
|
|
}</span>
|
|
|
|
// StartRecovery marks the account as recovering
|
|
func (a *Account) StartRecovery() error <span class="cov4" title="3">{
|
|
if !a.Status.CanInitiateRecovery() </span><span class="cov1" title="1">{
|
|
return ErrCannotInitiateRecovery
|
|
}</span>
|
|
<span class="cov3" title="2">a.Status = value_objects.AccountStatusRecovering
|
|
a.UpdatedAt = time.Now().UTC()
|
|
return nil</span>
|
|
}
|
|
|
|
// CompleteRecovery completes the recovery process with new public key
|
|
func (a *Account) CompleteRecovery(newPublicKey []byte, newKeygenSessionID uuid.UUID) <span class="cov1" title="1">{
|
|
a.PublicKey = newPublicKey
|
|
a.KeygenSessionID = newKeygenSessionID
|
|
a.Status = value_objects.AccountStatusActive
|
|
a.UpdatedAt = time.Now().UTC()
|
|
}</span>
|
|
|
|
// CanLogin checks if the account can login
|
|
func (a *Account) CanLogin() bool <span class="cov5" title="4">{
|
|
return a.Status.CanLogin()
|
|
}</span>
|
|
|
|
// IsActive checks if the account is active
|
|
func (a *Account) IsActive() bool <span class="cov0" title="0">{
|
|
return a.Status == value_objects.AccountStatusActive
|
|
}</span>
|
|
|
|
// Validate validates the account data
|
|
func (a *Account) Validate() error <span class="cov5" title="5">{
|
|
if a.Username == "" </span><span class="cov1" title="1">{
|
|
return ErrInvalidUsername
|
|
}</span>
|
|
<span class="cov5" title="4">if a.Email == "" </span><span class="cov1" title="1">{
|
|
return ErrInvalidEmail
|
|
}</span>
|
|
<span class="cov4" title="3">if len(a.PublicKey) == 0 </span><span class="cov1" title="1">{
|
|
return ErrInvalidPublicKey
|
|
}</span>
|
|
<span class="cov3" title="2">if a.ThresholdT > a.ThresholdN || a.ThresholdT <= 0 </span><span class="cov1" title="1">{
|
|
return ErrInvalidThreshold
|
|
}</span>
|
|
<span class="cov1" title="1">return nil</span>
|
|
}
|
|
|
|
// Account errors
|
|
var (
|
|
ErrInvalidUsername = &AccountError{Code: "INVALID_USERNAME", Message: "username is required"}
|
|
ErrInvalidEmail = &AccountError{Code: "INVALID_EMAIL", Message: "email is required"}
|
|
ErrInvalidPublicKey = &AccountError{Code: "INVALID_PUBLIC_KEY", Message: "public key is required"}
|
|
ErrInvalidThreshold = &AccountError{Code: "INVALID_THRESHOLD", Message: "invalid threshold configuration"}
|
|
ErrAccountInRecovery = &AccountError{Code: "ACCOUNT_IN_RECOVERY", Message: "account is in recovery mode"}
|
|
ErrCannotInitiateRecovery = &AccountError{Code: "CANNOT_INITIATE_RECOVERY", Message: "cannot initiate recovery in current state"}
|
|
ErrAccountNotActive = &AccountError{Code: "ACCOUNT_NOT_ACTIVE", Message: "account is not active"}
|
|
ErrAccountNotFound = &AccountError{Code: "ACCOUNT_NOT_FOUND", Message: "account not found"}
|
|
ErrDuplicateUsername = &AccountError{Code: "DUPLICATE_USERNAME", Message: "username already exists"}
|
|
ErrDuplicateEmail = &AccountError{Code: "DUPLICATE_EMAIL", Message: "email already exists"}
|
|
)
|
|
|
|
// AccountError represents an account domain error
|
|
type AccountError struct {
|
|
Code string
|
|
Message string
|
|
}
|
|
|
|
func (e *AccountError) Error() string <span class="cov0" title="0">{
|
|
return e.Message
|
|
}</span>
|
|
</pre>
|
|
|
|
<pre class="file" id="file4" style="display: none">package entities
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rwadurian/mpc-system/services/account/domain/value_objects"
|
|
)
|
|
|
|
// AccountShare represents a mapping of key share to account
|
|
// Note: This records share location, not share content
|
|
type AccountShare struct {
|
|
ID uuid.UUID
|
|
AccountID value_objects.AccountID
|
|
ShareType value_objects.ShareType
|
|
PartyID string
|
|
PartyIndex int
|
|
DeviceType *string
|
|
DeviceID *string
|
|
CreatedAt time.Time
|
|
LastUsedAt *time.Time
|
|
IsActive bool
|
|
}
|
|
|
|
// NewAccountShare creates a new AccountShare
|
|
func NewAccountShare(
|
|
accountID value_objects.AccountID,
|
|
shareType value_objects.ShareType,
|
|
partyID string,
|
|
partyIndex int,
|
|
) *AccountShare <span class="cov10" title="8">{
|
|
return &AccountShare{
|
|
ID: uuid.New(),
|
|
AccountID: accountID,
|
|
ShareType: shareType,
|
|
PartyID: partyID,
|
|
PartyIndex: partyIndex,
|
|
CreatedAt: time.Now().UTC(),
|
|
IsActive: true,
|
|
}
|
|
}</span>
|
|
|
|
// SetDeviceInfo sets device information for user device shares
|
|
func (s *AccountShare) SetDeviceInfo(deviceType, deviceID string) <span class="cov1" title="1">{
|
|
s.DeviceType = &deviceType
|
|
s.DeviceID = &deviceID
|
|
}</span>
|
|
|
|
// UpdateLastUsed updates the last used timestamp
|
|
func (s *AccountShare) UpdateLastUsed() <span class="cov0" title="0">{
|
|
now := time.Now().UTC()
|
|
s.LastUsedAt = &now
|
|
}</span>
|
|
|
|
// Deactivate deactivates the share (e.g., when device is lost)
|
|
func (s *AccountShare) Deactivate() <span class="cov1" title="1">{
|
|
s.IsActive = false
|
|
}</span>
|
|
|
|
// Activate activates the share
|
|
func (s *AccountShare) Activate() <span class="cov0" title="0">{
|
|
s.IsActive = true
|
|
}</span>
|
|
|
|
// IsUserDeviceShare checks if this is a user device share
|
|
func (s *AccountShare) IsUserDeviceShare() bool <span class="cov4" title="2">{
|
|
return s.ShareType == value_objects.ShareTypeUserDevice
|
|
}</span>
|
|
|
|
// IsServerShare checks if this is a server share
|
|
func (s *AccountShare) IsServerShare() bool <span class="cov5" title="3">{
|
|
return s.ShareType == value_objects.ShareTypeServer
|
|
}</span>
|
|
|
|
// IsRecoveryShare checks if this is a recovery share
|
|
func (s *AccountShare) IsRecoveryShare() bool <span class="cov1" title="1">{
|
|
return s.ShareType == value_objects.ShareTypeRecovery
|
|
}</span>
|
|
|
|
// Validate validates the account share
|
|
func (s *AccountShare) Validate() error <span class="cov4" title="2">{
|
|
if s.AccountID.IsZero() </span><span class="cov0" title="0">{
|
|
return ErrShareInvalidAccountID
|
|
}</span>
|
|
<span class="cov4" title="2">if !s.ShareType.IsValid() </span><span class="cov0" title="0">{
|
|
return ErrShareInvalidType
|
|
}</span>
|
|
<span class="cov4" title="2">if s.PartyID == "" </span><span class="cov1" title="1">{
|
|
return ErrShareInvalidPartyID
|
|
}</span>
|
|
<span class="cov1" title="1">if s.PartyIndex < 0 </span><span class="cov0" title="0">{
|
|
return ErrShareInvalidPartyIndex
|
|
}</span>
|
|
<span class="cov1" title="1">return nil</span>
|
|
}
|
|
|
|
// AccountShare errors
|
|
var (
|
|
ErrShareInvalidAccountID = &AccountError{Code: "SHARE_INVALID_ACCOUNT_ID", Message: "invalid account ID"}
|
|
ErrShareInvalidType = &AccountError{Code: "SHARE_INVALID_TYPE", Message: "invalid share type"}
|
|
ErrShareInvalidPartyID = &AccountError{Code: "SHARE_INVALID_PARTY_ID", Message: "invalid party ID"}
|
|
ErrShareInvalidPartyIndex = &AccountError{Code: "SHARE_INVALID_PARTY_INDEX", Message: "invalid party index"}
|
|
ErrShareNotFound = &AccountError{Code: "SHARE_NOT_FOUND", Message: "share not found"}
|
|
)
|
|
</pre>
|
|
|
|
<pre class="file" id="file5" style="display: none">package entities
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rwadurian/mpc-system/services/account/domain/value_objects"
|
|
)
|
|
|
|
// RecoverySession represents an account recovery session
|
|
type RecoverySession struct {
|
|
ID uuid.UUID
|
|
AccountID value_objects.AccountID
|
|
RecoveryType value_objects.RecoveryType
|
|
OldShareType *value_objects.ShareType
|
|
NewKeygenSessionID *uuid.UUID
|
|
Status value_objects.RecoveryStatus
|
|
RequestedAt time.Time
|
|
CompletedAt *time.Time
|
|
}
|
|
|
|
// NewRecoverySession creates a new RecoverySession
|
|
func NewRecoverySession(
|
|
accountID value_objects.AccountID,
|
|
recoveryType value_objects.RecoveryType,
|
|
) *RecoverySession <span class="cov10" title="5">{
|
|
return &RecoverySession{
|
|
ID: uuid.New(),
|
|
AccountID: accountID,
|
|
RecoveryType: recoveryType,
|
|
Status: value_objects.RecoveryStatusRequested,
|
|
RequestedAt: time.Now().UTC(),
|
|
}
|
|
}</span>
|
|
|
|
// SetOldShareType sets the old share type being replaced
|
|
func (r *RecoverySession) SetOldShareType(shareType value_objects.ShareType) <span class="cov0" title="0">{
|
|
r.OldShareType = &shareType
|
|
}</span>
|
|
|
|
// StartKeygen starts the keygen process for recovery
|
|
func (r *RecoverySession) StartKeygen(keygenSessionID uuid.UUID) error <span class="cov7" title="3">{
|
|
if r.Status != value_objects.RecoveryStatusRequested </span><span class="cov0" title="0">{
|
|
return ErrRecoveryInvalidState
|
|
}</span>
|
|
<span class="cov7" title="3">r.NewKeygenSessionID = &keygenSessionID
|
|
r.Status = value_objects.RecoveryStatusInProgress
|
|
return nil</span>
|
|
}
|
|
|
|
// Complete marks the recovery as completed
|
|
func (r *RecoverySession) Complete() error <span class="cov4" title="2">{
|
|
if r.Status != value_objects.RecoveryStatusInProgress </span><span class="cov0" title="0">{
|
|
return ErrRecoveryInvalidState
|
|
}</span>
|
|
<span class="cov4" title="2">now := time.Now().UTC()
|
|
r.CompletedAt = &now
|
|
r.Status = value_objects.RecoveryStatusCompleted
|
|
return nil</span>
|
|
}
|
|
|
|
// Fail marks the recovery as failed
|
|
func (r *RecoverySession) Fail() error <span class="cov4" title="2">{
|
|
if r.Status == value_objects.RecoveryStatusCompleted </span><span class="cov1" title="1">{
|
|
return ErrRecoveryAlreadyCompleted
|
|
}</span>
|
|
<span class="cov1" title="1">r.Status = value_objects.RecoveryStatusFailed
|
|
return nil</span>
|
|
}
|
|
|
|
// IsCompleted checks if recovery is completed
|
|
func (r *RecoverySession) IsCompleted() bool <span class="cov0" title="0">{
|
|
return r.Status == value_objects.RecoveryStatusCompleted
|
|
}</span>
|
|
|
|
// IsFailed checks if recovery failed
|
|
func (r *RecoverySession) IsFailed() bool <span class="cov0" title="0">{
|
|
return r.Status == value_objects.RecoveryStatusFailed
|
|
}</span>
|
|
|
|
// IsInProgress checks if recovery is in progress
|
|
func (r *RecoverySession) IsInProgress() bool <span class="cov0" title="0">{
|
|
return r.Status == value_objects.RecoveryStatusInProgress
|
|
}</span>
|
|
|
|
// Validate validates the recovery session
|
|
func (r *RecoverySession) Validate() error <span class="cov0" title="0">{
|
|
if r.AccountID.IsZero() </span><span class="cov0" title="0">{
|
|
return ErrRecoveryInvalidAccountID
|
|
}</span>
|
|
<span class="cov0" title="0">if !r.RecoveryType.IsValid() </span><span class="cov0" title="0">{
|
|
return ErrRecoveryInvalidType
|
|
}</span>
|
|
<span class="cov0" title="0">return nil</span>
|
|
}
|
|
|
|
// Recovery errors
|
|
var (
|
|
ErrRecoveryInvalidAccountID = &AccountError{Code: "RECOVERY_INVALID_ACCOUNT_ID", Message: "invalid account ID for recovery"}
|
|
ErrRecoveryInvalidType = &AccountError{Code: "RECOVERY_INVALID_TYPE", Message: "invalid recovery type"}
|
|
ErrRecoveryInvalidState = &AccountError{Code: "RECOVERY_INVALID_STATE", Message: "invalid recovery state for this operation"}
|
|
ErrRecoveryAlreadyCompleted = &AccountError{Code: "RECOVERY_ALREADY_COMPLETED", Message: "recovery already completed"}
|
|
ErrRecoveryNotFound = &AccountError{Code: "RECOVERY_NOT_FOUND", Message: "recovery session not found"}
|
|
)
|
|
</pre>
|
|
|
|
<pre class="file" id="file6" style="display: none">package value_objects
|
|
|
|
import (
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// AccountID represents a unique account identifier
|
|
type AccountID struct {
|
|
value uuid.UUID
|
|
}
|
|
|
|
// NewAccountID creates a new AccountID
|
|
func NewAccountID() AccountID <span class="cov10" title="34">{
|
|
return AccountID{value: uuid.New()}
|
|
}</span>
|
|
|
|
// AccountIDFromString creates an AccountID from a string
|
|
func AccountIDFromString(s string) (AccountID, error) <span class="cov2" title="2">{
|
|
id, err := uuid.Parse(s)
|
|
if err != nil </span><span class="cov1" title="1">{
|
|
return AccountID{}, err
|
|
}</span>
|
|
<span class="cov1" title="1">return AccountID{value: id}, nil</span>
|
|
}
|
|
|
|
// AccountIDFromUUID creates an AccountID from a UUID
|
|
func AccountIDFromUUID(id uuid.UUID) AccountID <span class="cov0" title="0">{
|
|
return AccountID{value: id}
|
|
}</span>
|
|
|
|
// String returns the string representation
|
|
func (id AccountID) String() string <span class="cov1" title="1">{
|
|
return id.value.String()
|
|
}</span>
|
|
|
|
// UUID returns the UUID value
|
|
func (id AccountID) UUID() uuid.UUID <span class="cov0" title="0">{
|
|
return id.value
|
|
}</span>
|
|
|
|
// IsZero checks if the AccountID is zero
|
|
func (id AccountID) IsZero() bool <span class="cov4" title="4">{
|
|
return id.value == uuid.Nil
|
|
}</span>
|
|
|
|
// Equals checks if two AccountIDs are equal
|
|
func (id AccountID) Equals(other AccountID) bool <span class="cov3" title="3">{
|
|
return id.value == other.value
|
|
}</span>
|
|
</pre>
|
|
|
|
<pre class="file" id="file7" style="display: none">package value_objects
|
|
|
|
// AccountStatus represents the status of an account
|
|
type AccountStatus string
|
|
|
|
const (
|
|
AccountStatusActive AccountStatus = "active"
|
|
AccountStatusSuspended AccountStatus = "suspended"
|
|
AccountStatusLocked AccountStatus = "locked"
|
|
AccountStatusRecovering AccountStatus = "recovering"
|
|
)
|
|
|
|
// String returns the string representation
|
|
func (s AccountStatus) String() string <span class="cov0" title="0">{
|
|
return string(s)
|
|
}</span>
|
|
|
|
// IsValid checks if the status is valid
|
|
func (s AccountStatus) IsValid() bool <span class="cov9" title="5">{
|
|
switch s </span>{
|
|
case AccountStatusActive, AccountStatusSuspended, AccountStatusLocked, AccountStatusRecovering:<span class="cov7" title="4">
|
|
return true</span>
|
|
default:<span class="cov1" title="1">
|
|
return false</span>
|
|
}
|
|
}
|
|
|
|
// CanLogin checks if the account can login with this status
|
|
func (s AccountStatus) CanLogin() bool <span class="cov7" title="4">{
|
|
return s == AccountStatusActive
|
|
}</span>
|
|
|
|
// CanInitiateRecovery checks if recovery can be initiated
|
|
func (s AccountStatus) CanInitiateRecovery() bool <span class="cov6" title="3">{
|
|
return s == AccountStatusActive || s == AccountStatusLocked
|
|
}</span>
|
|
|
|
// ShareType represents the type of key share
|
|
type ShareType string
|
|
|
|
const (
|
|
ShareTypeUserDevice ShareType = "user_device"
|
|
ShareTypeServer ShareType = "server"
|
|
ShareTypeRecovery ShareType = "recovery"
|
|
)
|
|
|
|
// String returns the string representation
|
|
func (st ShareType) String() string <span class="cov0" title="0">{
|
|
return string(st)
|
|
}</span>
|
|
|
|
// IsValid checks if the share type is valid
|
|
func (st ShareType) IsValid() bool <span class="cov10" title="6">{
|
|
switch st </span>{
|
|
case ShareTypeUserDevice, ShareTypeServer, ShareTypeRecovery:<span class="cov9" title="5">
|
|
return true</span>
|
|
default:<span class="cov1" title="1">
|
|
return false</span>
|
|
}
|
|
}
|
|
|
|
// RecoveryType represents the type of account recovery
|
|
type RecoveryType string
|
|
|
|
const (
|
|
RecoveryTypeDeviceLost RecoveryType = "device_lost"
|
|
RecoveryTypeShareRotation RecoveryType = "share_rotation"
|
|
)
|
|
|
|
// String returns the string representation
|
|
func (rt RecoveryType) String() string <span class="cov0" title="0">{
|
|
return string(rt)
|
|
}</span>
|
|
|
|
// IsValid checks if the recovery type is valid
|
|
func (rt RecoveryType) IsValid() bool <span class="cov0" title="0">{
|
|
switch rt </span>{
|
|
case RecoveryTypeDeviceLost, RecoveryTypeShareRotation:<span class="cov0" title="0">
|
|
return true</span>
|
|
default:<span class="cov0" title="0">
|
|
return false</span>
|
|
}
|
|
}
|
|
|
|
// RecoveryStatus represents the status of a recovery session
|
|
type RecoveryStatus string
|
|
|
|
const (
|
|
RecoveryStatusRequested RecoveryStatus = "requested"
|
|
RecoveryStatusInProgress RecoveryStatus = "in_progress"
|
|
RecoveryStatusCompleted RecoveryStatus = "completed"
|
|
RecoveryStatusFailed RecoveryStatus = "failed"
|
|
)
|
|
|
|
// String returns the string representation
|
|
func (rs RecoveryStatus) String() string <span class="cov0" title="0">{
|
|
return string(rs)
|
|
}</span>
|
|
|
|
// IsValid checks if the recovery status is valid
|
|
func (rs RecoveryStatus) IsValid() bool <span class="cov0" title="0">{
|
|
switch rs </span>{
|
|
case RecoveryStatusRequested, RecoveryStatusInProgress, RecoveryStatusCompleted, RecoveryStatusFailed:<span class="cov0" title="0">
|
|
return true</span>
|
|
default:<span class="cov0" title="0">
|
|
return false</span>
|
|
}
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file8" style="display: none">package entities
|
|
|
|
// DeviceType represents the type of device
|
|
type DeviceType string
|
|
|
|
const (
|
|
DeviceTypeAndroid DeviceType = "android"
|
|
DeviceTypeIOS DeviceType = "ios"
|
|
DeviceTypePC DeviceType = "pc"
|
|
DeviceTypeServer DeviceType = "server"
|
|
DeviceTypeRecovery DeviceType = "recovery"
|
|
)
|
|
|
|
// DeviceInfo holds information about a participant's device
|
|
type DeviceInfo struct {
|
|
DeviceType DeviceType `json:"device_type"`
|
|
DeviceID string `json:"device_id"`
|
|
Platform string `json:"platform"`
|
|
AppVersion string `json:"app_version"`
|
|
}
|
|
|
|
// NewDeviceInfo creates a new DeviceInfo
|
|
func NewDeviceInfo(deviceType DeviceType, deviceID, platform, appVersion string) DeviceInfo <span class="cov0" title="0">{
|
|
return DeviceInfo{
|
|
DeviceType: deviceType,
|
|
DeviceID: deviceID,
|
|
Platform: platform,
|
|
AppVersion: appVersion,
|
|
}
|
|
}</span>
|
|
|
|
// IsServer checks if the device is a server
|
|
func (d DeviceInfo) IsServer() bool <span class="cov0" title="0">{
|
|
return d.DeviceType == DeviceTypeServer
|
|
}</span>
|
|
|
|
// IsMobile checks if the device is mobile
|
|
func (d DeviceInfo) IsMobile() bool <span class="cov0" title="0">{
|
|
return d.DeviceType == DeviceTypeAndroid || d.DeviceType == DeviceTypeIOS
|
|
}</span>
|
|
|
|
// IsRecovery checks if the device is a recovery device
|
|
func (d DeviceInfo) IsRecovery() bool <span class="cov0" title="0">{
|
|
return d.DeviceType == DeviceTypeRecovery
|
|
}</span>
|
|
|
|
// Validate validates the device info
|
|
func (d DeviceInfo) Validate() error <span class="cov10" title="7">{
|
|
if d.DeviceType == "" </span><span class="cov0" title="0">{
|
|
return ErrInvalidDeviceInfo
|
|
}</span>
|
|
<span class="cov10" title="7">return nil</span>
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file9" style="display: none">package entities
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rwadurian/mpc-system/services/session-coordinator/domain/value_objects"
|
|
)
|
|
|
|
var (
|
|
ErrSessionFull = errors.New("session is full")
|
|
ErrSessionExpired = errors.New("session expired")
|
|
ErrSessionNotInProgress = errors.New("session not in progress")
|
|
ErrParticipantNotFound = errors.New("participant not found")
|
|
ErrInvalidSessionType = errors.New("invalid session type")
|
|
ErrInvalidStatusTransition = errors.New("invalid status transition")
|
|
)
|
|
|
|
// SessionType represents the type of MPC session
|
|
type SessionType string
|
|
|
|
const (
|
|
SessionTypeKeygen SessionType = "keygen"
|
|
SessionTypeSign SessionType = "sign"
|
|
)
|
|
|
|
// IsValid checks if the session type is valid
|
|
func (t SessionType) IsValid() bool <span class="cov10" title="7">{
|
|
return t == SessionTypeKeygen || t == SessionTypeSign
|
|
}</span>
|
|
|
|
// MPCSession represents an MPC session
|
|
// Coordinator only manages session metadata, does not participate in MPC computation
|
|
type MPCSession struct {
|
|
ID value_objects.SessionID
|
|
SessionType SessionType
|
|
Threshold value_objects.Threshold
|
|
Participants []*Participant
|
|
Status value_objects.SessionStatus
|
|
MessageHash []byte // Used for Sign sessions
|
|
PublicKey []byte // Group public key after Keygen completion
|
|
CreatedBy string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
ExpiresAt time.Time
|
|
CompletedAt *time.Time
|
|
}
|
|
|
|
// NewMPCSession creates a new MPC session
|
|
func NewMPCSession(
|
|
sessionType SessionType,
|
|
threshold value_objects.Threshold,
|
|
createdBy string,
|
|
expiresIn time.Duration,
|
|
messageHash []byte, // Only for Sign sessions
|
|
) (*MPCSession, error) <span class="cov10" title="7">{
|
|
if !sessionType.IsValid() </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidSessionType
|
|
}</span>
|
|
|
|
<span class="cov10" title="7">if sessionType == SessionTypeSign && len(messageHash) == 0 </span><span class="cov1" title="1">{
|
|
return nil, errors.New("message hash required for sign session")
|
|
}</span>
|
|
|
|
<span class="cov9" title="6">now := time.Now().UTC()
|
|
return &MPCSession{
|
|
ID: value_objects.NewSessionID(),
|
|
SessionType: sessionType,
|
|
Threshold: threshold,
|
|
Participants: make([]*Participant, 0, threshold.N()),
|
|
Status: value_objects.SessionStatusCreated,
|
|
MessageHash: messageHash,
|
|
CreatedBy: createdBy,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
ExpiresAt: now.Add(expiresIn),
|
|
}, nil</span>
|
|
}
|
|
|
|
// AddParticipant adds a participant to the session
|
|
func (s *MPCSession) AddParticipant(p *Participant) error <span class="cov7" title="4">{
|
|
if len(s.Participants) >= s.Threshold.N() </span><span class="cov1" title="1">{
|
|
return ErrSessionFull
|
|
}</span>
|
|
<span class="cov6" title="3">s.Participants = append(s.Participants, p)
|
|
s.UpdatedAt = time.Now().UTC()
|
|
return nil</span>
|
|
}
|
|
|
|
// GetParticipant gets a participant by party ID
|
|
func (s *MPCSession) GetParticipant(partyID value_objects.PartyID) (*Participant, error) <span class="cov0" title="0">{
|
|
for _, p := range s.Participants </span><span class="cov0" title="0">{
|
|
if p.PartyID.Equals(partyID) </span><span class="cov0" title="0">{
|
|
return p, nil
|
|
}</span>
|
|
}
|
|
<span class="cov0" title="0">return nil, ErrParticipantNotFound</span>
|
|
}
|
|
|
|
// UpdateParticipantStatus updates a participant's status
|
|
func (s *MPCSession) UpdateParticipantStatus(partyID value_objects.PartyID, status value_objects.ParticipantStatus) error <span class="cov0" title="0">{
|
|
for _, p := range s.Participants </span><span class="cov0" title="0">{
|
|
if p.PartyID.Equals(partyID) </span><span class="cov0" title="0">{
|
|
switch status </span>{
|
|
case value_objects.ParticipantStatusJoined:<span class="cov0" title="0">
|
|
return p.Join()</span>
|
|
case value_objects.ParticipantStatusReady:<span class="cov0" title="0">
|
|
return p.MarkReady()</span>
|
|
case value_objects.ParticipantStatusCompleted:<span class="cov0" title="0">
|
|
return p.MarkCompleted()</span>
|
|
case value_objects.ParticipantStatusFailed:<span class="cov0" title="0">
|
|
p.MarkFailed()
|
|
return nil</span>
|
|
default:<span class="cov0" title="0">
|
|
return errors.New("invalid status")</span>
|
|
}
|
|
}
|
|
}
|
|
<span class="cov0" title="0">return ErrParticipantNotFound</span>
|
|
}
|
|
|
|
// CanStart checks if all participants have joined and the session can start
|
|
func (s *MPCSession) CanStart() bool <span class="cov0" title="0">{
|
|
if len(s.Participants) != s.Threshold.N() </span><span class="cov0" title="0">{
|
|
return false
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">joinedCount := 0
|
|
for _, p := range s.Participants </span><span class="cov0" title="0">{
|
|
if p.IsJoined() </span><span class="cov0" title="0">{
|
|
joinedCount++
|
|
}</span>
|
|
}
|
|
<span class="cov0" title="0">return joinedCount == s.Threshold.N()</span>
|
|
}
|
|
|
|
// Start transitions the session to in_progress
|
|
func (s *MPCSession) Start() error <span class="cov0" title="0">{
|
|
if !s.Status.CanTransitionTo(value_objects.SessionStatusInProgress) </span><span class="cov0" title="0">{
|
|
return ErrInvalidStatusTransition
|
|
}</span>
|
|
<span class="cov0" title="0">if !s.CanStart() </span><span class="cov0" title="0">{
|
|
return errors.New("not all participants have joined")
|
|
}</span>
|
|
<span class="cov0" title="0">s.Status = value_objects.SessionStatusInProgress
|
|
s.UpdatedAt = time.Now().UTC()
|
|
return nil</span>
|
|
}
|
|
|
|
// Complete marks the session as completed
|
|
func (s *MPCSession) Complete(publicKey []byte) error <span class="cov0" title="0">{
|
|
if !s.Status.CanTransitionTo(value_objects.SessionStatusCompleted) </span><span class="cov0" title="0">{
|
|
return ErrInvalidStatusTransition
|
|
}</span>
|
|
<span class="cov0" title="0">s.Status = value_objects.SessionStatusCompleted
|
|
s.PublicKey = publicKey
|
|
now := time.Now().UTC()
|
|
s.CompletedAt = &now
|
|
s.UpdatedAt = now
|
|
return nil</span>
|
|
}
|
|
|
|
// Fail marks the session as failed
|
|
func (s *MPCSession) Fail() error <span class="cov0" title="0">{
|
|
if !s.Status.CanTransitionTo(value_objects.SessionStatusFailed) </span><span class="cov0" title="0">{
|
|
return ErrInvalidStatusTransition
|
|
}</span>
|
|
<span class="cov0" title="0">s.Status = value_objects.SessionStatusFailed
|
|
s.UpdatedAt = time.Now().UTC()
|
|
return nil</span>
|
|
}
|
|
|
|
// Expire marks the session as expired
|
|
func (s *MPCSession) Expire() error <span class="cov0" title="0">{
|
|
if !s.Status.CanTransitionTo(value_objects.SessionStatusExpired) </span><span class="cov0" title="0">{
|
|
return ErrInvalidStatusTransition
|
|
}</span>
|
|
<span class="cov0" title="0">s.Status = value_objects.SessionStatusExpired
|
|
s.UpdatedAt = time.Now().UTC()
|
|
return nil</span>
|
|
}
|
|
|
|
// IsExpired checks if the session has expired
|
|
func (s *MPCSession) IsExpired() bool <span class="cov4" title="2">{
|
|
return time.Now().UTC().After(s.ExpiresAt)
|
|
}</span>
|
|
|
|
// IsActive checks if the session is active
|
|
func (s *MPCSession) IsActive() bool <span class="cov0" title="0">{
|
|
return s.Status.IsActive() && !s.IsExpired()
|
|
}</span>
|
|
|
|
// IsParticipant checks if a party is a participant
|
|
func (s *MPCSession) IsParticipant(partyID value_objects.PartyID) bool <span class="cov0" title="0">{
|
|
for _, p := range s.Participants </span><span class="cov0" title="0">{
|
|
if p.PartyID.Equals(partyID) </span><span class="cov0" title="0">{
|
|
return true
|
|
}</span>
|
|
}
|
|
<span class="cov0" title="0">return false</span>
|
|
}
|
|
|
|
// AllCompleted checks if all participants have completed
|
|
func (s *MPCSession) AllCompleted() bool <span class="cov0" title="0">{
|
|
for _, p := range s.Participants </span><span class="cov0" title="0">{
|
|
if !p.IsCompleted() </span><span class="cov0" title="0">{
|
|
return false
|
|
}</span>
|
|
}
|
|
<span class="cov0" title="0">return true</span>
|
|
}
|
|
|
|
// CompletedCount returns the number of completed participants
|
|
func (s *MPCSession) CompletedCount() int <span class="cov0" title="0">{
|
|
count := 0
|
|
for _, p := range s.Participants </span><span class="cov0" title="0">{
|
|
if p.IsCompleted() </span><span class="cov0" title="0">{
|
|
count++
|
|
}</span>
|
|
}
|
|
<span class="cov0" title="0">return count</span>
|
|
}
|
|
|
|
// JoinedCount returns the number of joined participants
|
|
func (s *MPCSession) JoinedCount() int <span class="cov0" title="0">{
|
|
count := 0
|
|
for _, p := range s.Participants </span><span class="cov0" title="0">{
|
|
if p.IsJoined() </span><span class="cov0" title="0">{
|
|
count++
|
|
}</span>
|
|
}
|
|
<span class="cov0" title="0">return count</span>
|
|
}
|
|
|
|
// GetPartyIDs returns all party IDs
|
|
func (s *MPCSession) GetPartyIDs() []string <span class="cov0" title="0">{
|
|
ids := make([]string, len(s.Participants))
|
|
for i, p := range s.Participants </span><span class="cov0" title="0">{
|
|
ids[i] = p.PartyID.String()
|
|
}</span>
|
|
<span class="cov0" title="0">return ids</span>
|
|
}
|
|
|
|
// GetOtherParties returns participants except the specified party
|
|
func (s *MPCSession) GetOtherParties(excludePartyID value_objects.PartyID) []*Participant <span class="cov0" title="0">{
|
|
others := make([]*Participant, 0, len(s.Participants)-1)
|
|
for _, p := range s.Participants </span><span class="cov0" title="0">{
|
|
if !p.PartyID.Equals(excludePartyID) </span><span class="cov0" title="0">{
|
|
others = append(others, p)
|
|
}</span>
|
|
}
|
|
<span class="cov0" title="0">return others</span>
|
|
}
|
|
|
|
// ToDTO converts to a DTO for API responses
|
|
func (s *MPCSession) ToDTO() SessionDTO <span class="cov0" title="0">{
|
|
participants := make([]ParticipantDTO, len(s.Participants))
|
|
for i, p := range s.Participants </span><span class="cov0" title="0">{
|
|
participants[i] = ParticipantDTO{
|
|
PartyID: p.PartyID.String(),
|
|
PartyIndex: p.PartyIndex,
|
|
Status: p.Status.String(),
|
|
DeviceType: string(p.DeviceInfo.DeviceType),
|
|
}
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">return SessionDTO{
|
|
ID: s.ID.String(),
|
|
SessionType: string(s.SessionType),
|
|
ThresholdN: s.Threshold.N(),
|
|
ThresholdT: s.Threshold.T(),
|
|
Participants: participants,
|
|
Status: s.Status.String(),
|
|
CreatedAt: s.CreatedAt,
|
|
ExpiresAt: s.ExpiresAt,
|
|
}</span>
|
|
}
|
|
|
|
// SessionDTO is a data transfer object for sessions
|
|
type SessionDTO struct {
|
|
ID string `json:"id"`
|
|
SessionType string `json:"session_type"`
|
|
ThresholdN int `json:"threshold_n"`
|
|
ThresholdT int `json:"threshold_t"`
|
|
Participants []ParticipantDTO `json:"participants"`
|
|
Status string `json:"status"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
}
|
|
|
|
// ParticipantDTO is a data transfer object for participants
|
|
type ParticipantDTO struct {
|
|
PartyID string `json:"party_id"`
|
|
PartyIndex int `json:"party_index"`
|
|
Status string `json:"status"`
|
|
DeviceType string `json:"device_type"`
|
|
}
|
|
|
|
// Reconstruct reconstructs an MPCSession from database
|
|
func ReconstructSession(
|
|
id uuid.UUID,
|
|
sessionType string,
|
|
thresholdT, thresholdN int,
|
|
status string,
|
|
messageHash, publicKey []byte,
|
|
createdBy string,
|
|
createdAt, updatedAt, expiresAt time.Time,
|
|
completedAt *time.Time,
|
|
participants []*Participant,
|
|
) (*MPCSession, error) <span class="cov0" title="0">{
|
|
sessionStatus, err := value_objects.NewSessionStatus(status)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">threshold, err := value_objects.NewThreshold(thresholdT, thresholdN)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">return &MPCSession{
|
|
ID: value_objects.SessionIDFromUUID(id),
|
|
SessionType: SessionType(sessionType),
|
|
Threshold: threshold,
|
|
Participants: participants,
|
|
Status: sessionStatus,
|
|
MessageHash: messageHash,
|
|
PublicKey: publicKey,
|
|
CreatedBy: createdBy,
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: updatedAt,
|
|
ExpiresAt: expiresAt,
|
|
CompletedAt: completedAt,
|
|
}, nil</span>
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file10" style="display: none">package entities
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/rwadurian/mpc-system/services/session-coordinator/domain/value_objects"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidDeviceInfo = errors.New("invalid device info")
|
|
ErrParticipantNotInvited = errors.New("participant not in invited status")
|
|
ErrInvalidParticipant = errors.New("invalid participant")
|
|
)
|
|
|
|
// Participant represents a party in an MPC session
|
|
type Participant struct {
|
|
PartyID value_objects.PartyID
|
|
PartyIndex int
|
|
Status value_objects.ParticipantStatus
|
|
DeviceInfo DeviceInfo
|
|
PublicKey []byte // Party's identity public key (for authentication)
|
|
JoinedAt time.Time
|
|
CompletedAt *time.Time
|
|
}
|
|
|
|
// NewParticipant creates a new participant
|
|
func NewParticipant(partyID value_objects.PartyID, partyIndex int, deviceInfo DeviceInfo) (*Participant, error) <span class="cov10" title="7">{
|
|
if partyID.IsZero() </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidParticipant
|
|
}</span>
|
|
<span class="cov10" title="7">if partyIndex < 0 </span><span class="cov0" title="0">{
|
|
return nil, ErrInvalidParticipant
|
|
}</span>
|
|
<span class="cov10" title="7">if err := deviceInfo.Validate(); err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov10" title="7">return &Participant{
|
|
PartyID: partyID,
|
|
PartyIndex: partyIndex,
|
|
Status: value_objects.ParticipantStatusInvited,
|
|
DeviceInfo: deviceInfo,
|
|
JoinedAt: time.Now().UTC(),
|
|
}, nil</span>
|
|
}
|
|
|
|
// Join marks the participant as joined
|
|
func (p *Participant) Join() error <span class="cov1" title="1">{
|
|
if !p.Status.CanTransitionTo(value_objects.ParticipantStatusJoined) </span><span class="cov0" title="0">{
|
|
return errors.New("cannot transition to joined status")
|
|
}</span>
|
|
<span class="cov1" title="1">p.Status = value_objects.ParticipantStatusJoined
|
|
p.JoinedAt = time.Now().UTC()
|
|
return nil</span>
|
|
}
|
|
|
|
// MarkReady marks the participant as ready
|
|
func (p *Participant) MarkReady() error <span class="cov1" title="1">{
|
|
if !p.Status.CanTransitionTo(value_objects.ParticipantStatusReady) </span><span class="cov0" title="0">{
|
|
return errors.New("cannot transition to ready status")
|
|
}</span>
|
|
<span class="cov1" title="1">p.Status = value_objects.ParticipantStatusReady
|
|
return nil</span>
|
|
}
|
|
|
|
// MarkCompleted marks the participant as completed
|
|
func (p *Participant) MarkCompleted() error <span class="cov1" title="1">{
|
|
if !p.Status.CanTransitionTo(value_objects.ParticipantStatusCompleted) </span><span class="cov0" title="0">{
|
|
return errors.New("cannot transition to completed status")
|
|
}</span>
|
|
<span class="cov1" title="1">p.Status = value_objects.ParticipantStatusCompleted
|
|
now := time.Now().UTC()
|
|
p.CompletedAt = &now
|
|
return nil</span>
|
|
}
|
|
|
|
// MarkFailed marks the participant as failed
|
|
func (p *Participant) MarkFailed() <span class="cov1" title="1">{
|
|
p.Status = value_objects.ParticipantStatusFailed
|
|
}</span>
|
|
|
|
// IsJoined checks if the participant has joined
|
|
func (p *Participant) IsJoined() bool <span class="cov0" title="0">{
|
|
return p.Status == value_objects.ParticipantStatusJoined ||
|
|
p.Status == value_objects.ParticipantStatusReady ||
|
|
p.Status == value_objects.ParticipantStatusCompleted
|
|
}</span>
|
|
|
|
// IsReady checks if the participant is ready
|
|
func (p *Participant) IsReady() bool <span class="cov0" title="0">{
|
|
return p.Status == value_objects.ParticipantStatusReady ||
|
|
p.Status == value_objects.ParticipantStatusCompleted
|
|
}</span>
|
|
|
|
// IsCompleted checks if the participant has completed
|
|
func (p *Participant) IsCompleted() bool <span class="cov0" title="0">{
|
|
return p.Status == value_objects.ParticipantStatusCompleted
|
|
}</span>
|
|
|
|
// IsFailed checks if the participant has failed
|
|
func (p *Participant) IsFailed() bool <span class="cov0" title="0">{
|
|
return p.Status == value_objects.ParticipantStatusFailed
|
|
}</span>
|
|
|
|
// SetPublicKey sets the participant's public key
|
|
func (p *Participant) SetPublicKey(publicKey []byte) <span class="cov0" title="0">{
|
|
p.PublicKey = publicKey
|
|
}</span>
|
|
</pre>
|
|
|
|
<pre class="file" id="file11" style="display: none">package entities
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rwadurian/mpc-system/services/session-coordinator/domain/value_objects"
|
|
)
|
|
|
|
// SessionMessage represents an MPC message (encrypted, Coordinator does not decrypt)
|
|
type SessionMessage struct {
|
|
ID uuid.UUID
|
|
SessionID value_objects.SessionID
|
|
FromParty value_objects.PartyID
|
|
ToParties []value_objects.PartyID // nil means broadcast
|
|
RoundNumber int
|
|
MessageType string
|
|
Payload []byte // Encrypted MPC protocol message
|
|
CreatedAt time.Time
|
|
DeliveredAt *time.Time
|
|
}
|
|
|
|
// NewSessionMessage creates a new session message
|
|
func NewSessionMessage(
|
|
sessionID value_objects.SessionID,
|
|
fromParty value_objects.PartyID,
|
|
toParties []value_objects.PartyID,
|
|
roundNumber int,
|
|
messageType string,
|
|
payload []byte,
|
|
) *SessionMessage <span class="cov0" title="0">{
|
|
return &SessionMessage{
|
|
ID: uuid.New(),
|
|
SessionID: sessionID,
|
|
FromParty: fromParty,
|
|
ToParties: toParties,
|
|
RoundNumber: roundNumber,
|
|
MessageType: messageType,
|
|
Payload: payload,
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
}</span>
|
|
|
|
// IsBroadcast checks if the message is a broadcast
|
|
func (m *SessionMessage) IsBroadcast() bool <span class="cov0" title="0">{
|
|
return len(m.ToParties) == 0
|
|
}</span>
|
|
|
|
// IsFor checks if the message is for a specific party
|
|
func (m *SessionMessage) IsFor(partyID value_objects.PartyID) bool <span class="cov0" title="0">{
|
|
if m.IsBroadcast() </span><span class="cov0" title="0">{
|
|
// Broadcast is for everyone except sender
|
|
return !m.FromParty.Equals(partyID)
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">for _, to := range m.ToParties </span><span class="cov0" title="0">{
|
|
if to.Equals(partyID) </span><span class="cov0" title="0">{
|
|
return true
|
|
}</span>
|
|
}
|
|
<span class="cov0" title="0">return false</span>
|
|
}
|
|
|
|
// MarkDelivered marks the message as delivered
|
|
func (m *SessionMessage) MarkDelivered() <span class="cov0" title="0">{
|
|
now := time.Now().UTC()
|
|
m.DeliveredAt = &now
|
|
}</span>
|
|
|
|
// IsDelivered checks if the message has been delivered
|
|
func (m *SessionMessage) IsDelivered() bool <span class="cov0" title="0">{
|
|
return m.DeliveredAt != nil
|
|
}</span>
|
|
|
|
// GetToPartyStrings returns to parties as strings
|
|
func (m *SessionMessage) GetToPartyStrings() []string <span class="cov0" title="0">{
|
|
if m.IsBroadcast() </span><span class="cov0" title="0">{
|
|
return nil
|
|
}</span>
|
|
<span class="cov0" title="0">result := make([]string, len(m.ToParties))
|
|
for i, p := range m.ToParties </span><span class="cov0" title="0">{
|
|
result[i] = p.String()
|
|
}</span>
|
|
<span class="cov0" title="0">return result</span>
|
|
}
|
|
|
|
// ToDTO converts to a DTO
|
|
func (m *SessionMessage) ToDTO() MessageDTO <span class="cov0" title="0">{
|
|
toParties := m.GetToPartyStrings()
|
|
return MessageDTO{
|
|
ID: m.ID.String(),
|
|
SessionID: m.SessionID.String(),
|
|
FromParty: m.FromParty.String(),
|
|
ToParties: toParties,
|
|
IsBroadcast: m.IsBroadcast(),
|
|
RoundNumber: m.RoundNumber,
|
|
MessageType: m.MessageType,
|
|
Payload: m.Payload,
|
|
CreatedAt: m.CreatedAt,
|
|
}
|
|
}</span>
|
|
|
|
// MessageDTO is a data transfer object for messages
|
|
type MessageDTO struct {
|
|
ID string `json:"id"`
|
|
SessionID string `json:"session_id"`
|
|
FromParty string `json:"from_party"`
|
|
ToParties []string `json:"to_parties,omitempty"`
|
|
IsBroadcast bool `json:"is_broadcast"`
|
|
RoundNumber int `json:"round_number"`
|
|
MessageType string `json:"message_type"`
|
|
Payload []byte `json:"payload"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file12" style="display: none">package value_objects
|
|
|
|
import (
|
|
"errors"
|
|
"regexp"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidPartyID = errors.New("invalid party ID")
|
|
partyIDRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
|
|
)
|
|
|
|
// PartyID represents a unique party identifier
|
|
type PartyID struct {
|
|
value string
|
|
}
|
|
|
|
// NewPartyID creates a new PartyID
|
|
func NewPartyID(value string) (PartyID, error) <span class="cov10" title="12">{
|
|
if value == "" </span><span class="cov1" title="1">{
|
|
return PartyID{}, ErrInvalidPartyID
|
|
}</span>
|
|
<span class="cov9" title="11">if !partyIDRegex.MatchString(value) </span><span class="cov0" title="0">{
|
|
return PartyID{}, ErrInvalidPartyID
|
|
}</span>
|
|
<span class="cov9" title="11">if len(value) > 255 </span><span class="cov0" title="0">{
|
|
return PartyID{}, ErrInvalidPartyID
|
|
}</span>
|
|
<span class="cov9" title="11">return PartyID{value: value}, nil</span>
|
|
}
|
|
|
|
// MustNewPartyID creates a new PartyID, panics on error
|
|
func MustNewPartyID(value string) PartyID <span class="cov0" title="0">{
|
|
id, err := NewPartyID(value)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
panic(err)</span>
|
|
}
|
|
<span class="cov0" title="0">return id</span>
|
|
}
|
|
|
|
// String returns the string representation
|
|
func (id PartyID) String() string <span class="cov1" title="1">{
|
|
return id.value
|
|
}</span>
|
|
|
|
// IsZero checks if the PartyID is zero
|
|
func (id PartyID) IsZero() bool <span class="cov8" title="8">{
|
|
return id.value == ""
|
|
}</span>
|
|
|
|
// Equals checks if two PartyIDs are equal
|
|
func (id PartyID) Equals(other PartyID) bool <span class="cov3" title="2">{
|
|
return id.value == other.value
|
|
}</span>
|
|
</pre>
|
|
|
|
<pre class="file" id="file13" style="display: none">package value_objects
|
|
|
|
import (
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// SessionID represents a unique session identifier
|
|
type SessionID struct {
|
|
value uuid.UUID
|
|
}
|
|
|
|
// NewSessionID creates a new SessionID
|
|
func NewSessionID() SessionID <span class="cov10" title="8">{
|
|
return SessionID{value: uuid.New()}
|
|
}</span>
|
|
|
|
// SessionIDFromString creates a SessionID from a string
|
|
func SessionIDFromString(s string) (SessionID, error) <span class="cov4" title="2">{
|
|
id, err := uuid.Parse(s)
|
|
if err != nil </span><span class="cov1" title="1">{
|
|
return SessionID{}, err
|
|
}</span>
|
|
<span class="cov1" title="1">return SessionID{value: id}, nil</span>
|
|
}
|
|
|
|
// SessionIDFromUUID creates a SessionID from a UUID
|
|
func SessionIDFromUUID(id uuid.UUID) SessionID <span class="cov0" title="0">{
|
|
return SessionID{value: id}
|
|
}</span>
|
|
|
|
// String returns the string representation
|
|
func (id SessionID) String() string <span class="cov1" title="1">{
|
|
return id.value.String()
|
|
}</span>
|
|
|
|
// UUID returns the UUID value
|
|
func (id SessionID) UUID() uuid.UUID <span class="cov0" title="0">{
|
|
return id.value
|
|
}</span>
|
|
|
|
// IsZero checks if the SessionID is zero
|
|
func (id SessionID) IsZero() bool <span class="cov4" title="2">{
|
|
return id.value == uuid.Nil
|
|
}</span>
|
|
|
|
// Equals checks if two SessionIDs are equal
|
|
func (id SessionID) Equals(other SessionID) bool <span class="cov1" title="1">{
|
|
return id.value == other.value
|
|
}</span>
|
|
</pre>
|
|
|
|
<pre class="file" id="file14" style="display: none">package value_objects
|
|
|
|
import (
|
|
"errors"
|
|
)
|
|
|
|
var ErrInvalidSessionStatus = errors.New("invalid session status")
|
|
|
|
// SessionStatus represents the status of an MPC session
|
|
type SessionStatus string
|
|
|
|
const (
|
|
SessionStatusCreated SessionStatus = "created"
|
|
SessionStatusInProgress SessionStatus = "in_progress"
|
|
SessionStatusCompleted SessionStatus = "completed"
|
|
SessionStatusFailed SessionStatus = "failed"
|
|
SessionStatusExpired SessionStatus = "expired"
|
|
)
|
|
|
|
// ValidSessionStatuses contains all valid session statuses
|
|
var ValidSessionStatuses = []SessionStatus{
|
|
SessionStatusCreated,
|
|
SessionStatusInProgress,
|
|
SessionStatusCompleted,
|
|
SessionStatusFailed,
|
|
SessionStatusExpired,
|
|
}
|
|
|
|
// NewSessionStatus creates a new SessionStatus from string
|
|
func NewSessionStatus(s string) (SessionStatus, error) <span class="cov0" title="0">{
|
|
status := SessionStatus(s)
|
|
if !status.IsValid() </span><span class="cov0" title="0">{
|
|
return "", ErrInvalidSessionStatus
|
|
}</span>
|
|
<span class="cov0" title="0">return status, nil</span>
|
|
}
|
|
|
|
// String returns the string representation
|
|
func (s SessionStatus) String() string <span class="cov0" title="0">{
|
|
return string(s)
|
|
}</span>
|
|
|
|
// IsValid checks if the status is valid
|
|
func (s SessionStatus) IsValid() bool <span class="cov0" title="0">{
|
|
for _, valid := range ValidSessionStatuses </span><span class="cov0" title="0">{
|
|
if s == valid </span><span class="cov0" title="0">{
|
|
return true
|
|
}</span>
|
|
}
|
|
<span class="cov0" title="0">return false</span>
|
|
}
|
|
|
|
// CanTransitionTo checks if the status can transition to another
|
|
func (s SessionStatus) CanTransitionTo(target SessionStatus) bool <span class="cov0" title="0">{
|
|
transitions := map[SessionStatus][]SessionStatus{
|
|
SessionStatusCreated: {SessionStatusInProgress, SessionStatusFailed, SessionStatusExpired},
|
|
SessionStatusInProgress: {SessionStatusCompleted, SessionStatusFailed, SessionStatusExpired},
|
|
SessionStatusCompleted: {},
|
|
SessionStatusFailed: {},
|
|
SessionStatusExpired: {},
|
|
}
|
|
|
|
allowed, ok := transitions[s]
|
|
if !ok </span><span class="cov0" title="0">{
|
|
return false
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">for _, status := range allowed </span><span class="cov0" title="0">{
|
|
if status == target </span><span class="cov0" title="0">{
|
|
return true
|
|
}</span>
|
|
}
|
|
<span class="cov0" title="0">return false</span>
|
|
}
|
|
|
|
// IsTerminal checks if the status is terminal (cannot transition)
|
|
func (s SessionStatus) IsTerminal() bool <span class="cov0" title="0">{
|
|
return s == SessionStatusCompleted || s == SessionStatusFailed || s == SessionStatusExpired
|
|
}</span>
|
|
|
|
// IsActive checks if the session is active
|
|
func (s SessionStatus) IsActive() bool <span class="cov0" title="0">{
|
|
return s == SessionStatusCreated || s == SessionStatusInProgress
|
|
}</span>
|
|
|
|
// ParticipantStatus represents the status of a participant
|
|
type ParticipantStatus string
|
|
|
|
const (
|
|
ParticipantStatusInvited ParticipantStatus = "invited"
|
|
ParticipantStatusJoined ParticipantStatus = "joined"
|
|
ParticipantStatusReady ParticipantStatus = "ready"
|
|
ParticipantStatusCompleted ParticipantStatus = "completed"
|
|
ParticipantStatusFailed ParticipantStatus = "failed"
|
|
)
|
|
|
|
// ValidParticipantStatuses contains all valid participant statuses
|
|
var ValidParticipantStatuses = []ParticipantStatus{
|
|
ParticipantStatusInvited,
|
|
ParticipantStatusJoined,
|
|
ParticipantStatusReady,
|
|
ParticipantStatusCompleted,
|
|
ParticipantStatusFailed,
|
|
}
|
|
|
|
// String returns the string representation
|
|
func (s ParticipantStatus) String() string <span class="cov0" title="0">{
|
|
return string(s)
|
|
}</span>
|
|
|
|
// IsValid checks if the status is valid
|
|
func (s ParticipantStatus) IsValid() bool <span class="cov0" title="0">{
|
|
for _, valid := range ValidParticipantStatuses </span><span class="cov0" title="0">{
|
|
if s == valid </span><span class="cov0" title="0">{
|
|
return true
|
|
}</span>
|
|
}
|
|
<span class="cov0" title="0">return false</span>
|
|
}
|
|
|
|
// CanTransitionTo checks if the status can transition to another
|
|
func (s ParticipantStatus) CanTransitionTo(target ParticipantStatus) bool <span class="cov10" title="3">{
|
|
transitions := map[ParticipantStatus][]ParticipantStatus{
|
|
ParticipantStatusInvited: {ParticipantStatusJoined, ParticipantStatusFailed},
|
|
ParticipantStatusJoined: {ParticipantStatusReady, ParticipantStatusFailed},
|
|
ParticipantStatusReady: {ParticipantStatusCompleted, ParticipantStatusFailed},
|
|
ParticipantStatusCompleted: {},
|
|
ParticipantStatusFailed: {},
|
|
}
|
|
|
|
allowed, ok := transitions[s]
|
|
if !ok </span><span class="cov0" title="0">{
|
|
return false
|
|
}</span>
|
|
|
|
<span class="cov10" title="3">for _, status := range allowed </span><span class="cov10" title="3">{
|
|
if status == target </span><span class="cov10" title="3">{
|
|
return true
|
|
}</span>
|
|
}
|
|
<span class="cov0" title="0">return false</span>
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file15" style="display: none">package value_objects
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidThreshold = errors.New("invalid threshold")
|
|
ErrThresholdTooLarge = errors.New("threshold t cannot exceed n")
|
|
ErrThresholdTooSmall = errors.New("threshold t must be at least 1")
|
|
ErrNTooSmall = errors.New("n must be at least 2")
|
|
ErrNTooLarge = errors.New("n cannot exceed maximum allowed")
|
|
)
|
|
|
|
const (
|
|
MinN = 2
|
|
MaxN = 10
|
|
MinT = 1
|
|
)
|
|
|
|
// Threshold represents the t-of-n threshold configuration
|
|
type Threshold struct {
|
|
t int // Minimum number of parties required
|
|
n int // Total number of parties
|
|
}
|
|
|
|
// NewThreshold creates a new Threshold value object
|
|
func NewThreshold(t, n int) (Threshold, error) <span class="cov9" title="11">{
|
|
if n < MinN </span><span class="cov1" title="1">{
|
|
return Threshold{}, ErrNTooSmall
|
|
}</span>
|
|
<span class="cov9" title="10">if n > MaxN </span><span class="cov0" title="0">{
|
|
return Threshold{}, ErrNTooLarge
|
|
}</span>
|
|
<span class="cov9" title="10">if t < MinT </span><span class="cov1" title="1">{
|
|
return Threshold{}, ErrThresholdTooSmall
|
|
}</span>
|
|
<span class="cov8" title="9">if t > n </span><span class="cov1" title="1">{
|
|
return Threshold{}, ErrThresholdTooLarge
|
|
}</span>
|
|
<span class="cov8" title="8">return Threshold{t: t, n: n}, nil</span>
|
|
}
|
|
|
|
// MustNewThreshold creates a new Threshold, panics on error
|
|
func MustNewThreshold(t, n int) Threshold <span class="cov0" title="0">{
|
|
threshold, err := NewThreshold(t, n)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
panic(err)</span>
|
|
}
|
|
<span class="cov0" title="0">return threshold</span>
|
|
}
|
|
|
|
// T returns the minimum required parties
|
|
func (th Threshold) T() int <span class="cov3" title="2">{
|
|
return th.t
|
|
}</span>
|
|
|
|
// N returns the total parties
|
|
func (th Threshold) N() int <span class="cov10" title="12">{
|
|
return th.n
|
|
}</span>
|
|
|
|
// IsZero checks if the Threshold is zero
|
|
func (th Threshold) IsZero() bool <span class="cov1" title="1">{
|
|
return th.t == 0 && th.n == 0
|
|
}</span>
|
|
|
|
// Equals checks if two Thresholds are equal
|
|
func (th Threshold) Equals(other Threshold) bool <span class="cov0" title="0">{
|
|
return th.t == other.t && th.n == other.n
|
|
}</span>
|
|
|
|
// String returns the string representation
|
|
func (th Threshold) String() string <span class="cov0" title="0">{
|
|
return fmt.Sprintf("%d-of-%d", th.t, th.n)
|
|
}</span>
|
|
|
|
// CanSign checks if the given number of parties can sign
|
|
func (th Threshold) CanSign(availableParties int) bool <span class="cov0" title="0">{
|
|
return availableParties >= th.t
|
|
}</span>
|
|
|
|
// RequiresAllParties checks if all parties are required
|
|
func (th Threshold) RequiresAllParties() bool <span class="cov0" title="0">{
|
|
return th.t == th.n
|
|
}</span>
|
|
</pre>
|
|
|
|
</div>
|
|
</body>
|
|
<script>
|
|
(function() {
|
|
var files = document.getElementById('files');
|
|
var visible;
|
|
files.addEventListener('change', onChange, false);
|
|
function select(part) {
|
|
if (visible)
|
|
visible.style.display = 'none';
|
|
visible = document.getElementById(part);
|
|
if (!visible)
|
|
return;
|
|
files.value = part;
|
|
visible.style.display = 'block';
|
|
location.hash = part;
|
|
}
|
|
function onChange() {
|
|
select(files.value);
|
|
window.scrollTo(0, 0);
|
|
}
|
|
if (location.hash != "") {
|
|
select(location.hash.substr(1));
|
|
}
|
|
if (!visible) {
|
|
select("file0");
|
|
}
|
|
})();
|
|
</script>
|
|
</html>
|