Skip to content

Commit 178c22e

Browse files
authored
docs(cryptojwt): enhance API documentation with security considerations
docs(cryptojwt): enhance API documentation with security considerations
2 parents 4944f65 + e60372b commit 178c22e

3 files changed

Lines changed: 165 additions & 2 deletions

File tree

pkg/cryptojwt/esjwt.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@ type esjwtDecoderWithCachedPublicKey struct {
4242
}
4343

4444
// NewES256Encoder creates a new ECDSA-SHA256 JWT encoder with a private key file.
45+
//
46+
// Parameters:
47+
// - privateKeyFile: Path to PEM-encoded ECDSA private key file (P-256 curve)
48+
//
49+
// Security: Private key files should be protected with strict file permissions (0600).
50+
// Never commit private keys to version control. ECDSA keys are typically smaller than
51+
// RSA keys while providing equivalent security (256-bit ECDSA ≈ 3072-bit RSA).
52+
//
53+
// Example:
54+
//
55+
// encoder := cryptojwt.NewES256Encoder("ec-private.pem")
56+
// token, err := encoder.Encode(`{"user":"alice","exp":1735689600}`)
57+
// if err != nil {
58+
// log.Fatal(err)
59+
// }
4560
func NewES256Encoder(privateKeyFile string) Encoder {
4661
return &esjwtEncoderWithPrivateKeyFile{
4762
method: jwt.SigningMethodES256,
@@ -108,6 +123,21 @@ func NewES512DecoderWithPrivateKeyFileAndValidation(privateKeyFile string, valid
108123
}
109124

110125
// NewES256DecoderWithPublicKeyFile creates a new ECDSA-SHA256 JWT decoder with a public key file.
126+
//
127+
// Parameters:
128+
// - publicKeyFile: Path to PEM-encoded ECDSA public key file (P-256 curve)
129+
//
130+
// Note: Public keys can be safely distributed. Ensure you obtain public keys from
131+
// trusted sources to prevent signature validation bypasses.
132+
//
133+
// Example:
134+
//
135+
// decoder := cryptojwt.NewES256DecoderWithPublicKeyFile("ec-public.pem")
136+
// claims, err := decoder.Decode(token)
137+
// if err != nil {
138+
// log.Fatal(err)
139+
// }
140+
// fmt.Println(claims) // {"user":"alice","exp":1735689600}
111141
func NewES256DecoderWithPublicKeyFile(publicKeyFile string) Decoder {
112142
return NewES256DecoderWithPublicKeyFileAndValidation(publicKeyFile, ValidationOptions{})
113143
}
@@ -122,7 +152,25 @@ func NewES256DecoderWithPublicKeyFileAndValidation(publicKeyFile string, validat
122152
}
123153

124154
// NewES256EncoderWithCache creates a new ECDSA-SHA256 JWT encoder with cached private key.
125-
// The private key is loaded once at creation time, improving performance for repeated operations.
155+
//
156+
// The private key is loaded once at creation time, improving performance for repeated
157+
// operations. This is recommended for high-throughput scenarios.
158+
//
159+
// Security: The cached key remains in memory for the lifetime of the encoder. Private
160+
// key files should have strict file permissions (0600).
161+
//
162+
// Performance: Eliminates repeated file reads and key parsing for encoding many tokens.
163+
//
164+
// Example:
165+
//
166+
// encoder, err := cryptojwt.NewES256EncoderWithCache("ec-private.pem")
167+
// if err != nil {
168+
// log.Fatal(err)
169+
// }
170+
// for i := 0; i < 1000; i++ {
171+
// token, _ := encoder.Encode(fmt.Sprintf(`{"id":%d}`, i))
172+
// fmt.Println(token)
173+
// }
126174
func NewES256EncoderWithCache(privateKeyFile string) (Encoder, error) {
127175
privateKey, _, err := readECDSAPrivateKey(privateKeyFile)
128176
if err != nil {

pkg/cryptojwt/hsjwt.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,46 @@ func validateSecretLength(secret []byte, minLength int, algorithm string) error
3131
}
3232

3333
// NewHS256Encoder creates a new HMAC-SHA256 JWT encoder/decoder.
34+
//
35+
// Security: The secret should be at least 256 bits (32 bytes) for HS256.
36+
// Weak secrets are vulnerable to brute-force attacks. By default, this function
37+
// enforces minimum secret length according to RFC 7518. Use NewHS256EncoderWithOptions
38+
// with allowWeakSecret=true only for testing purposes.
39+
//
40+
// Example:
41+
//
42+
// secret := []byte("my-32-byte-secret-key-for-hs256")
43+
// encoder := cryptojwt.NewHS256Encoder(secret)
44+
// token, err := encoder.Encode(`{"user":"alice","exp":1735689600}`)
45+
// if err != nil {
46+
// log.Fatal(err)
47+
// }
48+
// fmt.Println(token) // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
3449
func NewHS256Encoder(secret []byte) EncoderDecoder {
3550
return NewHS256EncoderWithOptions(secret, false)
3651
}
3752

3853
// NewHS256EncoderWithOptions creates a new HMAC-SHA256 JWT encoder/decoder with options.
54+
//
55+
// Parameters:
56+
// - secret: The shared secret key (minimum 32 bytes recommended)
57+
// - allowWeakSecret: If true, allows secrets shorter than 32 bytes (TESTING ONLY)
58+
//
59+
// Security: Setting allowWeakSecret=true bypasses RFC 7518 security requirements.
60+
// Only use this for testing with non-production data.
3961
func NewHS256EncoderWithOptions(secret []byte, allowWeakSecret bool) EncoderDecoder {
4062
return NewHS256EncoderWithValidation(secret, allowWeakSecret, ValidationOptions{})
4163
}
4264

4365
// NewHS256EncoderWithValidation creates a new HMAC-SHA256 JWT encoder/decoder with validation options.
66+
//
67+
// Parameters:
68+
// - secret: The shared secret key (minimum 32 bytes recommended)
69+
// - allowWeakSecret: If true, allows secrets shorter than 32 bytes (TESTING ONLY)
70+
// - validationOpts: Options for validating JWT claims (exp, nbf, iat)
71+
//
72+
// Note: By default, time-based claims (exp, nbf, iat) are NOT validated.
73+
// Set validationOpts.ValidateClaims=true to enable automatic expiration checking.
4474
func NewHS256EncoderWithValidation(secret []byte, allowWeakSecret bool, validationOpts ValidationOptions) EncoderDecoder {
4575
return &hsjwtEncoderDecoder{
4676
method: jwt.SigningMethodHS256,
@@ -67,6 +97,10 @@ func NewHS256DecoderWithValidation(secret []byte, allowWeakSecret bool, validati
6797
}
6898

6999
// NewHS384Encoder creates a new HMAC-SHA384 JWT encoder/decoder.
100+
//
101+
// Security: The secret should be at least 384 bits (48 bytes) for HS384.
102+
// Weak secrets are vulnerable to brute-force attacks. By default, this function
103+
// enforces minimum secret length according to RFC 7518.
70104
func NewHS384Encoder(secret []byte) EncoderDecoder {
71105
return NewHS384EncoderWithOptions(secret, false)
72106
}
@@ -103,6 +137,10 @@ func NewHS384DecoderWithValidation(secret []byte, allowWeakSecret bool, validati
103137
}
104138

105139
// NewHS512Encoder creates a new HMAC-SHA512 JWT encoder/decoder.
140+
//
141+
// Security: The secret should be at least 512 bits (64 bytes) for HS512.
142+
// Weak secrets are vulnerable to brute-force attacks. By default, this function
143+
// enforces minimum secret length according to RFC 7518.
106144
func NewHS512Encoder(secret []byte) EncoderDecoder {
107145
return NewHS512EncoderWithOptions(secret, false)
108146
}
@@ -138,6 +176,17 @@ func NewHS512DecoderWithValidation(secret []byte, allowWeakSecret bool, validati
138176
return NewHS512EncoderWithValidation(secret, allowWeakSecret, validationOpts)
139177
}
140178

179+
// Decode validates and decodes a JWT token using HMAC algorithm.
180+
//
181+
// Security: This function validates that the token's algorithm matches the expected
182+
// HMAC algorithm to prevent algorithm confusion attacks. Tokens signed with different
183+
// algorithms will be rejected.
184+
//
185+
// Note: By default, time-based claims (exp, nbf, iat) are NOT validated. Use
186+
// NewHS*EncoderWithValidation with ValidationOptions.ValidateClaims=true to enable
187+
// automatic expiration checking.
188+
//
189+
// Returns: JSON string representation of the token's claims, or an error if validation fails.
141190
func (j *hsjwtEncoderDecoder) Decode(token string) (string, error) {
142191
if !j.allowWeakSecret {
143192
if err := j.validateSecret(); err != nil {
@@ -147,6 +196,19 @@ func (j *hsjwtEncoderDecoder) Decode(token string) (string, error) {
147196
return j.decoder.DecodeJWT(j.secret, token)
148197
}
149198

199+
// Encode creates and signs a JWT token using HMAC algorithm.
200+
//
201+
// The payload must be a valid JSON string that can be unmarshaled into jwt.MapClaims.
202+
//
203+
// Security: The returned token is signed but NOT encrypted. Do not include sensitive
204+
// data (passwords, API keys, PII) in the payload as it can be decoded by anyone.
205+
// Always transmit JWT tokens over HTTPS.
206+
//
207+
// Example payload:
208+
//
209+
// `{"user_id":"12345","role":"admin","exp":1735689600}`
210+
//
211+
// Returns: Base64-encoded JWT token (header.payload.signature) or an error if signing fails.
150212
func (j *hsjwtEncoderDecoder) Encode(payload string) (string, error) {
151213
if !j.allowWeakSecret {
152214
if err := j.validateSecret(); err != nil {

pkg/cryptojwt/rsjwt.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,21 @@ type rsjwtDecoderWithCachedPublicKey struct {
3939
}
4040

4141
// NewRS256Encoder creates a new RSA-SHA256 JWT encoder with a private key file.
42+
//
43+
// Parameters:
44+
// - privateKeyFile: Path to PEM-encoded RSA private key file
45+
//
46+
// Security: Private key files should be protected with strict file permissions (0600).
47+
// Never commit private keys to version control or expose them in logs. Consider using
48+
// environment variables or secure key management systems for production deployments.
49+
//
50+
// Example:
51+
//
52+
// encoder := cryptojwt.NewRS256Encoder("private.pem")
53+
// token, err := encoder.Encode(`{"user":"alice","exp":1735689600}`)
54+
// if err != nil {
55+
// log.Fatal(err)
56+
// }
4257
func NewRS256Encoder(privateKeyFile string) Encoder {
4358
return &rsjwtEncoderWithPrivateKeyFile{
4459
method: jwt.SigningMethodRS256,
@@ -61,6 +76,22 @@ func NewRS256DecoderWithPrivateKeyFileAndValidation(privateKeyFile string, valid
6176
}
6277

6378
// NewRS256DecoderWithPublicKeyFile creates a new RSA-SHA256 JWT decoder with a public key file.
79+
//
80+
// Parameters:
81+
// - publicKeyFile: Path to PEM-encoded RSA public key file
82+
//
83+
// Note: Public keys can be safely distributed and do not require special protection,
84+
// unlike private keys. However, ensure you obtain public keys from trusted sources to
85+
// prevent man-in-the-middle attacks.
86+
//
87+
// Example:
88+
//
89+
// decoder := cryptojwt.NewRS256DecoderWithPublicKeyFile("public.pem")
90+
// claims, err := decoder.Decode(token)
91+
// if err != nil {
92+
// log.Fatal(err)
93+
// }
94+
// fmt.Println(claims) // {"user":"alice","exp":1735689600}
6495
func NewRS256DecoderWithPublicKeyFile(publicKeyFile string) Decoder {
6596
return NewRS256DecoderWithPublicKeyFileAndValidation(publicKeyFile, ValidationOptions{})
6697
}
@@ -75,7 +106,29 @@ func NewRS256DecoderWithPublicKeyFileAndValidation(publicKeyFile string, validat
75106
}
76107

77108
// NewRS256EncoderWithCache creates a new RSA-SHA256 JWT encoder with cached private key.
78-
// The private key is loaded once at creation time, improving performance for repeated operations.
109+
//
110+
// The private key is loaded once at creation time, improving performance for repeated
111+
// operations. This is recommended for high-throughput scenarios where you need to encode
112+
// many tokens without repeated file I/O.
113+
//
114+
// Security: The cached key remains in memory for the lifetime of the encoder. Ensure
115+
// proper memory protection in production environments. Private key files should have
116+
// strict file permissions (0600).
117+
//
118+
// Performance: For applications encoding thousands of tokens, this can provide significant
119+
// performance improvements by eliminating repeated file reads and key parsing.
120+
//
121+
// Example:
122+
//
123+
// encoder, err := cryptojwt.NewRS256EncoderWithCache("private.pem")
124+
// if err != nil {
125+
// log.Fatal(err)
126+
// }
127+
// // Encode many tokens efficiently
128+
// for i := 0; i < 1000; i++ {
129+
// token, _ := encoder.Encode(fmt.Sprintf(`{"id":%d}`, i))
130+
// fmt.Println(token)
131+
// }
79132
func NewRS256EncoderWithCache(privateKeyFile string) (Encoder, error) {
80133
privateKey, _, err := readPrivateRSAKey(privateKeyFile)
81134
if err != nil {

0 commit comments

Comments
 (0)