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}