11package xyz .srnyx .javautilities .objects .encryptor ;
22
3+ import com .google .gson .JsonElement ;
4+ import com .google .gson .JsonObject ;
5+ import com .google .gson .JsonPrimitive ;
6+
37import org .jetbrains .annotations .NotNull ;
48import org .jetbrains .annotations .Nullable ;
59
610import xyz .srnyx .javautilities .manipulation .Mapper ;
711import xyz .srnyx .javautilities .objects .encryptor .exceptions .TokenExpiredException ;
812import xyz .srnyx .javautilities .objects .encryptor .exceptions .TokenInvalidException ;
9- import xyz .srnyx .javautilities .objects .encryptor .exceptions .TokenTamperedException ;
1013
11- import javax .crypto .Mac ;
14+ import javax .crypto .Cipher ;
15+ import javax .crypto .NoSuchPaddingException ;
16+ import javax .crypto .spec .GCMParameterSpec ;
1217import javax .crypto .spec .SecretKeySpec ;
18+ import java .nio .ByteBuffer ;
1319import java .nio .charset .StandardCharsets ;
20+ import java .security .InvalidAlgorithmParameterException ;
1421import java .security .InvalidKeyException ;
1522import java .security .NoSuchAlgorithmException ;
23+ import java .security .SecureRandom ;
1624import java .time .Duration ;
17- import java .util .Arrays ;
1825import java .util .Base64 ;
19- import java .util .Optional ;
2026
2127
2228/**
2329 * Utility class for encrypting and decrypting values using HMAC signatures
2430 */
2531public class Encryptor {
2632 /**
27- * The algorithm used for signing
33+ * The algorithm used for encryption
34+ * <br>AES (Advanced Encryption Standard) is a symmetric encryption algorithm widely used across the globe
35+ * <br>It is known for its speed and security, making it suitable for encrypting sensitive data
36+ */
37+ @ NotNull private static final String ALGORITHM = "AES" ;
38+ /**
39+ * The transformation string for AES in GCM mode with NoPadding
40+ * <br>GCM (Galois/Counter Mode) is a mode of operation for symmetric key cryptographic block ciphers
41+ * <br>It provides both confidentiality and data integrity
2842 */
29- @ NotNull private final String algorithm ;
43+ @ NotNull private static final String TRANSFORMATION = "AES/GCM/NoPadding" ;
44+ /**
45+ * The size of the initialization vector (IV) in bits
46+ * <br>GCM typically uses a 96-bit IV, which is recommended for security
47+ * <br>Using a unique IV for each encryption operation is crucial to prevent replay attacks
48+ */
49+ private static final int IV_SIZE = 12 ; // 96 bits for GCM
50+ /**
51+ * The size of the authentication tag in bits
52+ * <br>GCM uses a 128-bit tag for authentication, which is standard and provides a good balance between security and performance
53+ * <br>The tag is used to verify the integrity of the encrypted data
54+ */
55+ private static final int TAG_SIZE = 128 ;
56+
3057 /**
3158 * The secret key used for signing
3259 */
@@ -40,94 +67,123 @@ public class Encryptor {
4067 /**
4168 * Creates a new {@link Encryptor}
4269 *
43- * @param algorithm {@link #algorithm}
4470 * @param secret {@link #secret}
4571 * @param maxAge {@link #maxAge}
4672 *
4773 * @throws NoSuchAlgorithmException if the specified algorithm is not available
4874 * @throws InvalidKeyException if the provided secret is invalid
75+ * @throws NoSuchPaddingException if the specified padding scheme is not available
4976 */
50- public Encryptor (@ NotNull String algorithm , @ NotNull byte [] secret , @ Nullable Duration maxAge ) throws NoSuchAlgorithmException , InvalidKeyException {
51- this .algorithm = algorithm ;
52- this .secret = new SecretKeySpec (secret , algorithm );
77+ public Encryptor (@ NotNull byte [] secret , @ Nullable Duration maxAge ) throws NoSuchAlgorithmException , InvalidKeyException , NoSuchPaddingException , InvalidAlgorithmParameterException {
78+ this .secret = new SecretKeySpec (secret , ALGORITHM );
5379 this .maxAge = maxAge ;
5480
55- // Test Mac instance for validity
56- final Mac mac = Mac . getInstance ( algorithm );
57- mac . init (this .secret );
81+ // Validate
82+ if ( maxAge != null && maxAge . isNegative ()) throw new IllegalArgumentException ( "maxAge cannot be negative" );
83+ Cipher . getInstance ( TRANSFORMATION ). init (Cipher . ENCRYPT_MODE , this .secret , new GCMParameterSpec ( TAG_SIZE , new byte [ IV_SIZE ]) );
5884 }
5985
6086 /**
61- * Generates a signature for the given payload
87+ * Generates the cipher text for encryption or decryption
6288 *
63- * @param payload the payload to sign
89+ * @param mode the mode of operation ({@link Cipher#ENCRYPT_MODE} or {@link Cipher#DECRYPT_MODE})
90+ * @param iv the initialization vector (IV) used for GCM mode
91+ * @param token the token to encrypt or decrypt
6492 *
65- * @return the signature , or {@code null} if an error occurred
93+ * @return the resulting cipher text as a byte array , or null if an error occurs
6694 */
6795 @ Nullable
68- private byte [] getSignature ( @ NotNull String payload ) {
96+ private byte [] getCipherText ( int mode , byte [] iv , byte [] token ) {
6997 try {
70- final Mac mac = Mac .getInstance (algorithm );
71- mac .init (secret );
72- return mac .doFinal (payload .getBytes (StandardCharsets .UTF_8 ));
98+ final Cipher cipher = Cipher .getInstance (TRANSFORMATION );
99+ final GCMParameterSpec spec = new GCMParameterSpec (TAG_SIZE , iv );
100+ cipher .init (mode , secret , spec );
101+ return cipher .doFinal (token );
73102 } catch (final Exception e ) {
74- // This should never happen since we test the Mac instance in the constructor
75103 e .printStackTrace ();
76104 return null ;
77105 }
78106 }
79107
80108 /**
81- * Encrypts a value by creating a signed token that includes the value and a timestamp
82- * <br>The token format is {@code base64(value:timestamp:signature)}
109+ * Encrypts a {@link JsonElement} value and returns a Base64 URL-safe string without padding
83110 *
84- * @param value the value to encrypt, will be converted to string using {@link Object#toString()}
111+ * @param value the {@link JsonElement} value to encrypt
85112 *
86- * @return the encrypted token, or {@code null} if an error occurred during signature generation
113+ * @return the encrypted token as a Base64 URL-safe string without padding, or null if encryption fails
87114 */
88115 @ Nullable
89- public String encrypt (@ NotNull Object value ) {
90- final String payload = value + ":" + System .currentTimeMillis ();
91- final byte [] signature = getSignature (payload );
92- if (signature == null ) return null ; // Should never happen
93- final String token = payload + ":" + Base64 .getUrlEncoder ().withoutPadding ().encodeToString (signature );
94- return Base64 .getUrlEncoder ().withoutPadding ().encodeToString (token .getBytes (StandardCharsets .UTF_8 ));
116+ public String encrypt (@ NotNull JsonElement value ) {
117+ // Validate value
118+ if (value .isJsonNull ()) return null ;
119+
120+ // Generate random IV
121+ final byte [] iv = new byte [IV_SIZE ];
122+ new SecureRandom ().nextBytes (iv );
123+
124+ // Create payload with timestamp and value
125+ final JsonObject payload = new JsonObject ();
126+ payload .addProperty ("timestamp" , String .valueOf (System .currentTimeMillis ())); // Store as string to prevent rounding
127+ payload .add ("value" , value );
128+
129+ // Encrypt payload
130+ final byte [] ciphertext = getCipherText (Cipher .ENCRYPT_MODE , iv , payload .toString ().getBytes (StandardCharsets .UTF_8 ));
131+ if (ciphertext == null ) return null ;
132+
133+ // Combine IV and ciphertext
134+ final ByteBuffer buffer = ByteBuffer .allocate (IV_SIZE + ciphertext .length );
135+ buffer .put (iv );
136+ buffer .put (ciphertext );
137+
138+ // Encode to Base64 URL-safe string without padding
139+ return Base64 .getUrlEncoder ().withoutPadding ().encodeToString (buffer .array ());
95140 }
96141
97142 /**
98- * Decrypts a token by verifying its signature and timestamp
99- *
100- * @param token the token to decrypt
143+ * Decrypts a Base64 URL-safe string token and returns the original {@link JsonElement} value
101144 *
102- * @return the original value if the token is valid and not expired, otherwise {@code null}
145+ * @param token the Base64 URL-safe string token to decrypt
103146 *
104- * @throws TokenInvalidException if the token is invalid
105- * @throws TokenExpiredException if the token has expired
106- * @throws TokenTamperedException if the token has been tampered with
147+ * @return the decrypted {@link JsonElement} value, or null if decryption fails or token is invalid
107148 */
108149 @ Nullable
109- public String decrypt (@ NotNull String token ) throws TokenInvalidException , TokenExpiredException , TokenTamperedException {
110- // Decode token
111- final String decoded = new String (Base64 .getUrlDecoder ().decode (token ), StandardCharsets .UTF_8 );
112- final String [] parts = decoded .split (":" );
113- if (parts .length != 3 ) throw new TokenInvalidException ("Token does not have 3 parts" );
114-
115- // Get casted value and timestamp
116- final String value = parts [0 ];
117- if (value == null ) throw new TokenInvalidException ("Value is null" );
118- final Optional <Long > timestamp = Mapper .toLong (parts [1 ]);
119- if (!timestamp .isPresent ()) throw new TokenInvalidException ("Timestamp is not a valid long" );
120-
121- // Check age
122- if (maxAge != null && System .currentTimeMillis () - timestamp .get () > maxAge .toMillis ()) throw new TokenExpiredException ();
123-
124- // Recompute signature
125- final String payload = parts [0 ] + ":" + parts [1 ];
126- final byte [] expectedSig = getSignature (payload );
127- if (expectedSig == null ) return null ; // Should never happen
128- final byte [] actualSig = Base64 .getUrlDecoder ().decode (parts [2 ]);
129- if (!Arrays .equals (expectedSig , actualSig )) throw new TokenTamperedException ();
130-
131- return value ;
150+ public JsonElement decrypt (@ NotNull String token ) throws TokenExpiredException , TokenInvalidException {
151+ // Validate token format
152+ if (token .isEmpty ()) throw new TokenInvalidException ("Token is empty or invalid" );
153+
154+ // Decode Base64 URL-safe string
155+ final byte [] decoded = Base64 .getUrlDecoder ().decode (token );
156+ final ByteBuffer buffer = ByteBuffer .wrap (decoded );
157+
158+ // Get IV from beginning of buffer
159+ final byte [] iv = new byte [IV_SIZE ];
160+ buffer .get (iv );
161+ if (buffer .remaining () < 1 ) throw new TokenInvalidException ("Token is invalid or tampered with, no cipherText found" );
162+
163+ // Extract cipherText
164+ final byte [] cipherText = new byte [buffer .remaining ()];
165+ buffer .get (cipherText );
166+
167+ // Decrypt cipherText
168+ final byte [] decrypted = getCipherText (Cipher .DECRYPT_MODE , iv , cipherText );
169+ if (decrypted == null ) throw new TokenInvalidException ("Decryption failed, token is invalid or tampered with" );
170+ final String decryptedString = new String (decrypted , StandardCharsets .UTF_8 );
171+
172+ // Parse JSON
173+ final JsonObject jsonObject = Mapper .toJson (decryptedString )
174+ .flatMap (element -> Mapper .convertJsonElement (element , JsonObject .class ))
175+ .orElseThrow (() -> new TokenInvalidException ("Decrypted token is not a valid JSON object" ));
176+ if (!jsonObject .has ("timestamp" ) || !jsonObject .has ("value" )) throw new TokenInvalidException ("Decrypted token is missing required fields" );
177+
178+ // Check timestamp expiration
179+ final String timestamp = Mapper .convertJsonElement (jsonObject .get ("timestamp" ), JsonPrimitive .class )
180+ .flatMap (primitive -> Mapper .convertJsonPrimitive (primitive , String .class ))
181+ .orElseThrow (() -> new TokenInvalidException ("Timestamp is not a valid string" ));
182+ final Long timestampValue = Mapper .toLong (timestamp ).orElseThrow (() -> new TokenInvalidException ("Timestamp is not a valid long value" ));
183+ if (maxAge != null && System .currentTimeMillis () - timestampValue > maxAge .toMillis ()) throw new TokenExpiredException ();
184+
185+ // Return value
186+ final JsonElement value = jsonObject .get ("value" );
187+ return value .isJsonNull () ? null : value ;
132188 }
133189}
0 commit comments