001/*
002 * Copyright 2025 devteam@scivics-lab.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing,
011 * software distributed under the License is distributed on an
012 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
013 * either express or implied.  See the License for the
014 * specific language governing permissions and limitations
015 * under the License.
016 */
017
018package com.scivicslab.actoriac;
019
020import javax.crypto.Cipher;
021import javax.crypto.KeyGenerator;
022import javax.crypto.SecretKey;
023import javax.crypto.spec.GCMParameterSpec;
024import javax.crypto.spec.SecretKeySpec;
025import java.nio.ByteBuffer;
026import java.nio.charset.StandardCharsets;
027import java.security.SecureRandom;
028import java.util.Base64;
029
030/**
031 * Utility for encrypting and decrypting secrets using AES-256-GCM.
032 *
033 * <p>This class provides authenticated encryption with AES-256 in GCM mode,
034 * which provides both confidentiality and integrity protection.</p>
035 *
036 * <h2>Usage Example</h2>
037 * <pre>{@code
038 * // Generate a new encryption key
039 * String key = SecretEncryptor.generateKey();
040 * System.out.println("ACTOR_IAC_SECRET_KEY=" + key);
041 *
042 * // Encrypt a file
043 * String plaintext = Files.readString(Path.of("secrets.ini"));
044 * String encrypted = SecretEncryptor.encrypt(plaintext, key);
045 * Files.writeString(Path.of("secrets.enc"), encrypted);
046 *
047 * // Decrypt a file
048 * String encryptedContent = Files.readString(Path.of("secrets.enc"));
049 * String decrypted = SecretEncryptor.decrypt(encryptedContent, key);
050 * }</pre>
051 *
052 * @author devteam@scivics-lab.com
053 */
054public class SecretEncryptor {
055
056    private static final String ALGORITHM = "AES";
057    private static final String TRANSFORMATION = "AES/GCM/NoPadding";
058    private static final int KEY_SIZE = 256;
059    private static final int GCM_IV_LENGTH = 12; // 96 bits
060    private static final int GCM_TAG_LENGTH = 128; // 128 bits
061
062    /**
063     * Generates a new random encryption key.
064     *
065     * @return Base64-encoded encryption key
066     * @throws EncryptionException if key generation fails
067     */
068    public static String generateKey() throws EncryptionException {
069        try {
070            KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);
071            keyGenerator.init(KEY_SIZE, new SecureRandom());
072            SecretKey secretKey = keyGenerator.generateKey();
073            return Base64.getEncoder().encodeToString(secretKey.getEncoded());
074        } catch (Exception e) {
075            throw new EncryptionException("Failed to generate encryption key", e);
076        }
077    }
078
079    /**
080     * Encrypts plaintext using AES-256-GCM.
081     *
082     * @param plaintext the text to encrypt
083     * @param base64Key Base64-encoded encryption key
084     * @return Base64-encoded encrypted data (IV + ciphertext + tag)
085     * @throws EncryptionException if encryption fails
086     */
087    public static String encrypt(String plaintext, String base64Key) throws EncryptionException {
088        try {
089            byte[] keyBytes = Base64.getDecoder().decode(base64Key);
090            SecretKey secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
091
092            // Generate random IV
093            byte[] iv = new byte[GCM_IV_LENGTH];
094            new SecureRandom().nextBytes(iv);
095
096            // Initialize cipher
097            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
098            GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
099            cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec);
100
101            // Encrypt
102            byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
103            byte[] ciphertext = cipher.doFinal(plaintextBytes);
104
105            // Combine IV + ciphertext
106            ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + ciphertext.length);
107            byteBuffer.put(iv);
108            byteBuffer.put(ciphertext);
109
110            // Return Base64-encoded result
111            return Base64.getEncoder().encodeToString(byteBuffer.array());
112
113        } catch (Exception e) {
114            throw new EncryptionException("Failed to encrypt data", e);
115        }
116    }
117
118    /**
119     * Decrypts encrypted data using AES-256-GCM.
120     *
121     * @param encryptedBase64 Base64-encoded encrypted data (IV + ciphertext + tag)
122     * @param base64Key Base64-encoded encryption key
123     * @return decrypted plaintext
124     * @throws EncryptionException if decryption fails
125     */
126    public static String decrypt(String encryptedBase64, String base64Key) throws EncryptionException {
127        try {
128            byte[] keyBytes = Base64.getDecoder().decode(base64Key);
129            SecretKey secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
130
131            // Decode encrypted data
132            byte[] encryptedData = Base64.getDecoder().decode(encryptedBase64);
133
134            // Extract IV and ciphertext
135            ByteBuffer byteBuffer = ByteBuffer.wrap(encryptedData);
136            byte[] iv = new byte[GCM_IV_LENGTH];
137            byteBuffer.get(iv);
138            byte[] ciphertext = new byte[byteBuffer.remaining()];
139            byteBuffer.get(ciphertext);
140
141            // Initialize cipher
142            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
143            GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
144            cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec);
145
146            // Decrypt
147            byte[] plaintextBytes = cipher.doFinal(ciphertext);
148            return new String(plaintextBytes, StandardCharsets.UTF_8);
149
150        } catch (Exception e) {
151            throw new EncryptionException("Failed to decrypt data", e);
152        }
153    }
154
155    /**
156     * Exception thrown when encryption/decryption operations fail.
157     */
158    public static class EncryptionException extends Exception {
159        public EncryptionException(String message, Throwable cause) {
160            super(message, cause);
161        }
162    }
163}