A community based topic aggregation platform built on atproto
1package oauth 2 3import ( 4 "crypto/aes" 5 "crypto/cipher" 6 "crypto/rand" 7 "encoding/base64" 8 "encoding/json" 9 "fmt" 10 "time" 11) 12 13// SealedSession represents the data sealed in a mobile session token 14type SealedSession struct { 15 DID string `json:"did"` // User's DID 16 SessionID string `json:"sid"` // Session identifier 17 ExpiresAt int64 `json:"exp"` // Unix timestamp when token expires 18} 19 20// SealSession creates an encrypted token containing session information. 21// The token is encrypted using AES-256-GCM and encoded as base64url. 22// 23// Token format: base64url(nonce || ciphertext || tag) 24// - nonce: 12 bytes (GCM standard nonce size) 25// - ciphertext: encrypted JSON payload 26// - tag: 16 bytes (GCM authentication tag) 27// 28// The sealed token can be safely given to mobile clients and used as 29// a reference to the server-side session without exposing sensitive data. 30func (c *OAuthClient) SealSession(did, sessionID string, ttl time.Duration) (string, error) { 31 if len(c.SealSecret) == 0 { 32 return "", fmt.Errorf("seal secret not configured") 33 } 34 35 if did == "" { 36 return "", fmt.Errorf("DID is required") 37 } 38 39 if sessionID == "" { 40 return "", fmt.Errorf("session ID is required") 41 } 42 43 // Create the session data 44 expiresAt := time.Now().Add(ttl).Unix() 45 session := SealedSession{ 46 DID: did, 47 SessionID: sessionID, 48 ExpiresAt: expiresAt, 49 } 50 51 // Marshal to JSON 52 plaintext, err := json.Marshal(session) 53 if err != nil { 54 return "", fmt.Errorf("failed to marshal session: %w", err) 55 } 56 57 // Create AES cipher 58 block, err := aes.NewCipher(c.SealSecret) 59 if err != nil { 60 return "", fmt.Errorf("failed to create cipher: %w", err) 61 } 62 63 // Create GCM mode 64 gcm, err := cipher.NewGCM(block) 65 if err != nil { 66 return "", fmt.Errorf("failed to create GCM: %w", err) 67 } 68 69 // Generate random nonce 70 nonce := make([]byte, gcm.NonceSize()) 71 if _, err := rand.Read(nonce); err != nil { 72 return "", fmt.Errorf("failed to generate nonce: %w", err) 73 } 74 75 // Encrypt and authenticate 76 // GCM.Seal appends the ciphertext and tag to the nonce 77 ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) 78 79 // Encode as base64url (no padding) 80 token := base64.RawURLEncoding.EncodeToString(ciphertext) 81 82 return token, nil 83} 84 85// UnsealSession decrypts and validates a sealed session token. 86// Returns the session information if the token is valid and not expired. 87func (c *OAuthClient) UnsealSession(token string) (*SealedSession, error) { 88 if len(c.SealSecret) == 0 { 89 return nil, fmt.Errorf("seal secret not configured") 90 } 91 92 if token == "" { 93 return nil, fmt.Errorf("token is required") 94 } 95 96 // Decode from base64url 97 ciphertext, err := base64.RawURLEncoding.DecodeString(token) 98 if err != nil { 99 return nil, fmt.Errorf("invalid token encoding: %w", err) 100 } 101 102 // Create AES cipher 103 block, err := aes.NewCipher(c.SealSecret) 104 if err != nil { 105 return nil, fmt.Errorf("failed to create cipher: %w", err) 106 } 107 108 // Create GCM mode 109 gcm, err := cipher.NewGCM(block) 110 if err != nil { 111 return nil, fmt.Errorf("failed to create GCM: %w", err) 112 } 113 114 // Verify minimum size (nonce + tag) 115 nonceSize := gcm.NonceSize() 116 if len(ciphertext) < nonceSize { 117 return nil, fmt.Errorf("invalid token: too short") 118 } 119 120 // Extract nonce and ciphertext 121 nonce := ciphertext[:nonceSize] 122 ciphertextData := ciphertext[nonceSize:] 123 124 // Decrypt and authenticate 125 plaintext, err := gcm.Open(nil, nonce, ciphertextData, nil) 126 if err != nil { 127 return nil, fmt.Errorf("failed to decrypt token: %w", err) 128 } 129 130 // Unmarshal JSON 131 var session SealedSession 132 if err := json.Unmarshal(plaintext, &session); err != nil { 133 return nil, fmt.Errorf("failed to unmarshal session: %w", err) 134 } 135 136 // Validate required fields 137 if session.DID == "" { 138 return nil, fmt.Errorf("invalid session: missing DID") 139 } 140 141 if session.SessionID == "" { 142 return nil, fmt.Errorf("invalid session: missing session ID") 143 } 144 145 // Check expiration 146 now := time.Now().Unix() 147 if session.ExpiresAt <= now { 148 return nil, fmt.Errorf("token expired at %v", time.Unix(session.ExpiresAt, 0)) 149 } 150 151 return &session, nil 152}