Input/output (IO) operations are foundational to nearly every Java application, enabling programs to read and write files, communicate over networks, and serialize data for storage or transmission. However, IO operations also introduce significant security risks if not handled carefully. Attackers often exploit insecure IO practices to gain unauthorized access, execute malicious code, or compromise data integrity and confidentiality.
This introduction explores why IO is a common source of security vulnerabilities in Java applications, highlights fundamental security principles like least privilege and input validation, and provides examples of common IO vulnerabilities alongside mitigation techniques.
IO operations in Java interact with external resources outside the JVM’s internal control—such as the filesystem, network sockets, and external data streams. These interactions inherently increase the attack surface:
Since IO often deals with untrusted data—files uploaded by users, network input, or serialized objects—any lapse in validating or securing these operations can lead to serious security breaches.
Limit the permissions of your Java application and its components to the minimum necessary for their IO tasks. For example:
Least privilege reduces the potential impact if an attacker exploits an IO vulnerability.
All data read through IO channels must be treated as untrusted. Rigorously validate and sanitize input before processing:
Reject or sanitize malformed or unexpected data to prevent injection attacks, buffer overflows, or crashes.
Java IO APIs often have default behaviors that may not be secure in all contexts. Adopt secure defaults such as:
Description: An attacker crafts a filename containing sequences like ../
to access files outside the intended directory.
Example:
String baseDir = "/app/data/";
String requestedFile = "../../etc/passwd";
File file = new File(baseDir, requestedFile);
If unchecked, this may read the system’s password file.
Mitigation:
File#getCanonicalPath()
to resolve the absolute path.File requested = new File(baseDir, requestedFile);
String canonicalBase = new File(baseDir).getCanonicalPath();
String canonicalRequested = requested.getCanonicalPath();
if (!canonicalRequested.startsWith(canonicalBase)) {
throw new SecurityException("Invalid file path");
}
Description: Data sent over plaintext sockets can be intercepted or altered by attackers.
Mitigation:
SSLSocket
or HttpsURLConnection
.SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
// Secure communication
}
Description: Java deserialization allows reconstruction of objects from byte streams. Attackers can craft malicious byte streams to execute arbitrary code during deserialization.
Mitigation:
ObjectInputFilter
(Java 9+).ObjectInputStream ois = new ObjectInputStream(inputStream);
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("com.example.MySafeClass;!*");
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();
Description: Poor IO handling can cause resource leaks, such as open file descriptors or sockets, leading to denial of service.
Mitigation:
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
// Read file safely
}
Principle | Best Practice |
---|---|
Least Privilege | Limit filesystem and network permissions |
Input Validation | Sanitize paths, verify data formats |
Secure Defaults | Use encrypted communication and safe deserialization |
Resource Management | Close IO resources promptly with try-with-resources |
IO operations in Java are inherently risky due to their interaction with external resources and untrusted data. Following security principles—least privilege, rigorous input validation, secure defaults, and proper resource management—greatly reduces the attack surface.
Common vulnerabilities like directory traversal, insecure network communication, unsafe deserialization, and resource exhaustion can be mitigated through careful coding and the use of modern Java security features.
By adopting these security-conscious IO practices, Java developers can build applications that are robust, reliable, and resistant to common IO-related attacks.
File access is a common yet sensitive operation in Java applications. Improper handling of file permissions can lead to unauthorized reading, modification, or deletion of critical data, exposing the application and its environment to security risks. Securing file access means ensuring that your Java program only accesses files it is authorized to, and that files are protected against unintended or malicious changes.
This section covers how to check and enforce file permissions using Java’s File
class and the more advanced java.nio.file.attribute
package, the role of security managers in access control, and practical coding techniques to prevent unauthorized file operations.
Java provides multiple layers for managing file access:
File
ClassThe java.io.File
class offers simple methods to check file permissions:
canRead()
— Returns true
if the file is readable.canWrite()
— Returns true
if the file is writable.canExecute()
— Returns true
if the file is executable.File file = new File("/path/to/file.txt");
if (file.exists()) {
System.out.println("File exists");
System.out.println("Readable: " + file.canRead());
System.out.println("Writable: " + file.canWrite());
System.out.println("Executable: " + file.canExecute());
} else {
System.out.println("File does not exist.");
}
While these methods help to inspect permissions, they are often insufficient for enforcing strict security policies because they reflect the current OS permissions, and you may want more fine-grained control or to modify permissions programmatically.
java.nio.file.attribute
Java 7 introduced the java.nio.file
package, including the attribute
subpackage, which provides a more flexible and powerful way to inspect and modify file attributes and permissions.
PosixFilePermission
On POSIX-compliant systems (Linux, Unix, macOS), you can read and change file permissions using PosixFilePermission
:
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.Set;
Path path = Paths.get("/path/to/file.txt");
// Read permissions
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(path);
System.out.println("Current permissions: " + PosixFilePermissions.toString(perms));
// Add owner write permission
perms.add(PosixFilePermission.OWNER_WRITE);
// Remove group write permission
perms.remove(PosixFilePermission.GROUP_WRITE);
// Set the new permissions
Files.setPosixFilePermissions(path, perms);
System.out.println("Permissions updated.");
This approach allows you to enforce minimum necessary permissions explicitly and prevent unwanted access.
On Windows, the AclFileAttributeView
can be used to manipulate Access Control Lists (ACLs):
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.List;
Path path = Paths.get("C:\\path\\to\\file.txt");
AclFileAttributeView aclView = Files.getFileAttributeView(path, AclFileAttributeView.class);
List<AclEntry> aclEntries = aclView.getAcl();
for (AclEntry entry : aclEntries) {
System.out.println(entry.principal() + ": " + entry.permissions());
}
// Modify ACLs as needed (requires detailed knowledge of Windows ACL)
This allows precise control over which users or groups can access or modify the file.
Note: The SecurityManager
and its associated file permission checks are deprecated and slated for removal in future Java versions, but understanding them is helpful in legacy contexts.
The SecurityManager
can restrict file operations by enforcing policies based on java.io.FilePermission
. For example, you can configure a security policy file to grant or deny read/write access to specific files or directories.
Example security policy entry:
grant codeBase "file:/myapp/-" {
permission java.io.FilePermission "/myapp/data/-", "read,write";
permission java.io.FilePermission "/myapp/config/config.xml", "read";
};
With the Security Manager enabled, Java checks these permissions before allowing file operations, throwing SecurityException
if denied.
Validate and sanitize file paths: Prevent directory traversal by canonicalizing paths and restricting access to allowed directories.
File baseDir = new File("/app/data/");
File requested = new File(baseDir, userInputPath);
String canonicalBase = baseDir.getCanonicalPath();
String canonicalRequested = requested.getCanonicalPath();
if (!canonicalRequested.startsWith(canonicalBase)) {
throw new SecurityException("Access denied: invalid file path");
}
Check permissions before operations: Use File.canRead()
and File.canWrite()
to verify access, but don’t rely solely on these—handle exceptions robustly.
Set secure file permissions after creating files: For example, create a file and then restrict access to owner only.
Path newFile = Files.createFile(Paths.get("/app/data/newfile.txt"));
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------");
Files.setPosixFilePermissions(newFile, perms);
Use try-with-resources and handle exceptions: Ensure streams and channels close properly to avoid resource leaks that might cause file locks.
Avoid running as an administrator or root: Run your Java process under a least-privilege OS user to limit potential damage.
Here is an example demonstrating secure reading of a user-requested file, with path validation and permission checking:
import java.io.*;
import java.nio.file.*;
public class SecureFileReader {
private final Path baseDirectory;
public SecureFileReader(String baseDir) {
this.baseDirectory = Paths.get(baseDir).toAbsolutePath().normalize();
}
public String readFile(String userProvidedPath) throws IOException {
Path requestedFile = baseDirectory.resolve(userProvidedPath).normalize();
if (!requestedFile.startsWith(baseDirectory)) {
throw new SecurityException("Unauthorized file access attempt");
}
if (!Files.exists(requestedFile) || !Files.isReadable(requestedFile)) {
throw new FileNotFoundException("File not found or not readable");
}
try (BufferedReader reader = Files.newBufferedReader(requestedFile)) {
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
return content.toString();
}
}
public static void main(String[] args) {
try {
SecureFileReader sfr = new SecureFileReader("/app/data");
String content = sfr.readFile("example.txt");
System.out.println("File content:\n" + content);
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
}
}
This code ensures the requested file resides under a base directory, checks read permissions, and safely reads the file.
Securing file access in Java requires a combination of OS-level permissions and application-level checks. The File
class provides simple permission inspection, while the java.nio.file.attribute
package offers advanced control over file attributes and access rights. Although the Security Manager can enforce security policies, modern applications rely more on OS user permissions and explicit permission handling.
By validating file paths, verifying permissions before access, setting restrictive file attributes, and carefully managing file IO operations, you can greatly reduce the risk of unauthorized file access and modification in your Java applications.
Network communication in Java NIO offers scalable, non-blocking IO operations, which are ideal for high-performance servers and clients. However, by default, NIO channels transmit data in plaintext, exposing them to eavesdropping, tampering, and man-in-the-middle attacks. To secure NIO-based networking, Java provides the SSLEngine
API, which allows integrating SSL/TLS encryption and authentication while preserving the non-blocking nature of NIO.
This section explains how to implement secure network communication using Java NIO and SSLEngine
. We cover:
SSLEngine
in TLS integration.SocketChannel
with SSLEngine
.SSLEngine
for TLS in Java NIO?Traditional SSL/TLS support in Java (e.g., SSLSocket
) is blocking and tightly coupled to socket IO. SSLEngine
separates TLS protocol handling from actual IO and is designed for integration with non-blocking transport layers like SocketChannel
. It allows developers to:
SSLEngine
Works with NIOThe key concept is that SSLEngine
handles TLS protocol logic on byte buffers:
The developer drives the handshake and data processing by repeatedly calling wrap()
and unwrap()
methods on SSLEngine
, moving data between these buffers and the underlying SocketChannel
.
SocketChannel
and SSLEngine
First, create an SSLContext
initialized with key and trust managers, then create an SSLEngine
configured as client or server.
import javax.net.ssl.*;
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null);
SSLEngine sslEngine = sslContext.createSSLEngine(host, port);
sslEngine.setUseClientMode(true); // or false if server
Here, keyManagers
and trustManagers
manage certificates and trust chains.
You must allocate buffers for outgoing (encrypted) and incoming (encrypted) network data, and for outgoing and incoming application (plaintext) data.
SSLSession session = sslEngine.getSession();
ByteBuffer appData = ByteBuffer.allocate(session.getApplicationBufferSize());
ByteBuffer netData = ByteBuffer.allocate(session.getPacketBufferSize());
ByteBuffer peerAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
ByteBuffer peerNetData = ByteBuffer.allocate(session.getPacketBufferSize());
appData
: plaintext data to send.netData
: encrypted data to send.peerNetData
: encrypted data received from network.peerAppData
: decrypted data received from peer.Create and configure the SocketChannel
for non-blocking mode and connect it.
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(host, port));
// Use Selector to wait for connect completion and subsequent read/write readiness.
The handshake involves several SSLEngineResult.HandshakeStatus
states and requires driving wrap()
and unwrap()
calls:
sslEngine.beginHandshake();
SSLEngineResult.HandshakeStatus handshakeStatus = sslEngine.getHandshakeStatus();
while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED &&
handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
switch (handshakeStatus) {
case NEED_UNWRAP:
if (socketChannel.read(peerNetData) < 0) {
throw new IOException("Channel closed during handshake");
}
peerNetData.flip();
SSLEngineResult unwrapResult = sslEngine.unwrap(peerNetData, peerAppData);
peerNetData.compact();
handshakeStatus = unwrapResult.getHandshakeStatus();
break;
case NEED_WRAP:
netData.clear();
SSLEngineResult wrapResult = sslEngine.wrap(appData, netData);
handshakeStatus = wrapResult.getHandshakeStatus();
netData.flip();
while (netData.hasRemaining()) {
socketChannel.write(netData);
}
break;
case NEED_TASK:
Runnable task;
while ((task = sslEngine.getDelegatedTask()) != null) {
task.run();
}
handshakeStatus = sslEngine.getHandshakeStatus();
break;
default:
throw new IllegalStateException("Invalid handshake status: " + handshakeStatus);
}
}
After a successful handshake, application data can be exchanged securely using wrap()
and unwrap()
:
Sending data:
appData.clear();
appData.put("Hello secure world".getBytes(StandardCharsets.UTF_8));
appData.flip();
netData.clear();
SSLEngineResult result = sslEngine.wrap(appData, netData);
netData.flip();
while (netData.hasRemaining()) {
socketChannel.write(netData);
}
Receiving data:
peerNetData.clear();
int bytesRead = socketChannel.read(peerNetData);
if (bytesRead > 0) {
peerNetData.flip();
peerAppData.clear();
SSLEngineResult result = sslEngine.unwrap(peerNetData, peerAppData);
peerNetData.compact();
peerAppData.flip();
byte[] receivedBytes = new byte[peerAppData.remaining()];
peerAppData.get(receivedBytes);
System.out.println("Received: " + new String(receivedBytes, StandardCharsets.UTF_8));
}
The wrap()
method encrypts plaintext data into TLS packets, and unwrap()
decrypts received TLS packets into plaintext.
Here's a complete, single runnable Java example that:
SocketChannel
and SSLEngine
⚠️ Note: For a real secure deployment, you'd load proper certificates. This example uses a dummy trust manager that accepts all certificates for simplicity.
import javax.net.ssl.*;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
public class SecureNioClient {
public static void main(String[] args) throws Exception {
String host = "localhost";
int port = 8443;
// Step 1: Create SSLContext with dummy TrustManager (INSECURE - for demo only)
TrustManager[] trustAll = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return null; }
public void checkClientTrusted(X509Certificate[] certs, String authType) { }
public void checkServerTrusted(X509Certificate[] certs, String authType) { }
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAll, new SecureRandom());
SSLEngine sslEngine = sslContext.createSSLEngine(host, port);
sslEngine.setUseClientMode(true);
sslEngine.beginHandshake();
SSLSession session = sslEngine.getSession();
// Step 2: Allocate Buffers
ByteBuffer appData = ByteBuffer.allocate(session.getApplicationBufferSize());
ByteBuffer netData = ByteBuffer.allocate(session.getPacketBufferSize());
ByteBuffer peerAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
ByteBuffer peerNetData = ByteBuffer.allocate(session.getPacketBufferSize());
// Step 3: Open SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(host, port));
while (!socketChannel.finishConnect()) {
Thread.sleep(50); // Wait for connection
}
// Step 4: TLS Handshake
SSLEngineResult.HandshakeStatus handshakeStatus = sslEngine.getHandshakeStatus();
while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED &&
handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
switch (handshakeStatus) {
case NEED_UNWRAP:
if (socketChannel.read(peerNetData) < 0) {
throw new IOException("Channel closed during handshake");
}
peerNetData.flip();
SSLEngineResult unwrapResult = sslEngine.unwrap(peerNetData, peerAppData);
peerNetData.compact();
handshakeStatus = unwrapResult.getHandshakeStatus();
break;
case NEED_WRAP:
netData.clear();
SSLEngineResult wrapResult = sslEngine.wrap(ByteBuffer.allocate(0), netData);
handshakeStatus = wrapResult.getHandshakeStatus();
netData.flip();
while (netData.hasRemaining()) {
socketChannel.write(netData);
}
break;
case NEED_TASK:
Runnable task;
while ((task = sslEngine.getDelegatedTask()) != null) {
task.run();
}
handshakeStatus = sslEngine.getHandshakeStatus();
break;
default:
throw new IllegalStateException("Unexpected handshake status: " + handshakeStatus);
}
}
System.out.println("TLS Handshake completed.");
// Step 5: Send Secure Data
String message = "Hello secure world!";
appData.clear();
appData.put(message.getBytes(StandardCharsets.UTF_8));
appData.flip();
netData.clear();
SSLEngineResult wrapResult = sslEngine.wrap(appData, netData);
netData.flip();
while (netData.hasRemaining()) {
socketChannel.write(netData);
}
System.out.println("Sent: " + message);
// Step 6: Receive Secure Response
peerNetData.clear();
int bytesRead = socketChannel.read(peerNetData);
if (bytesRead > 0) {
peerNetData.flip();
peerAppData.clear();
SSLEngineResult result = sslEngine.unwrap(peerNetData, peerAppData);
peerNetData.compact();
peerAppData.flip();
byte[] receivedBytes = new byte[peerAppData.remaining()];
peerAppData.get(receivedBytes);
System.out.println("Received: " + new String(receivedBytes, StandardCharsets.UTF_8));
}
// Cleanup
sslEngine.closeOutbound();
socketChannel.close();
}
}
localhost:8443
(can be a simple echo server)This demo uses a dummy X509TrustManager
that accepts all certificates, which is insecure and should never be used in production. Always validate certificates using a properly configured trust store.
sslEngine.getSession()
to avoid buffer overflows or underflows.BUFFER_OVERFLOW
and BUFFER_UNDERFLOW
: Check SSLEngineResult.Status
and adjust buffer sizes or read more data accordingly.SSLException
to gracefully close connections on failure.sslEngine.closeOutbound()
and perform a proper SSL/TLS close_notify handshake.// 1. Setup SSLContext, SSLEngine (client mode)
// 2. Create non-blocking SocketChannel and connect
// 3. Perform handshake loop as described
// 4. After handshake, wrap application data and write to channel
// 5. Read encrypted data, unwrap, and process plaintext
// 6. Close connection gracefully with SSL/TLS close_notify
Integrating SSL/TLS into Java NIO applications requires explicit management of encrypted and decrypted buffers via the SSLEngine
API. While this adds complexity compared to traditional blocking SSL sockets, it enables scalable, secure network applications with non-blocking IO.
The process involves:
SSLEngine
.Mastering SSLEngine
unlocks the ability to build high-performance, secure Java servers and clients that benefit from both TLS security and NIO scalability.
Java IO streams are essential for reading and writing data, but handling sensitive information—like passwords, encryption keys, or personal user data—requires extra care. Without proper precautions, sensitive data may be inadvertently exposed through plaintext storage, memory leaks, or insecure transmission channels.
This guide covers best practices for securely handling sensitive data in Java IO streams, focusing on:
CipherInputStream
and CipherOutputStream
.Sensitive data such as passwords, API keys, credit card information, or personally identifiable information (PII) are attractive targets for attackers. Common risks when handling sensitive data via IO streams include:
Following secure IO practices reduces these risks and helps comply with privacy regulations and security standards.
Never store raw passwords, keys, or tokens in plaintext files. If persistent storage is necessary:
Encrypt sensitive data with a robust symmetric cipher (e.g., AES) before writing it to disk or sending over the network. Decrypt only when necessary.
javax.crypto
).Buffers holding sensitive data in memory should be cleared immediately after use to prevent lingering secrets:
String
) for sensitive data, since they cannot be erased from memory.CipherInputStream
and CipherOutputStream
for Stream EncryptionThese classes allow you to transparently encrypt or decrypt data as it flows through Java IO streams.
Here’s how to implement stream encryption and decryption securely with CipherOutputStream
and CipherInputStream
.
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import java.io.*;
import java.security.SecureRandom;
public class SecureStreamExample {
private static final String AES = "AES";
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 16; // bytes
private static final int GCM_IV_LENGTH = 12; // bytes
// Generate a random AES key
public static SecretKey generateKey() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance(AES);
keyGen.init(256); // Use 256-bit AES key (requires JCE Unlimited Strength)
return keyGen.generateKey();
}
// Generate a secure random IV (nonce)
public static byte[] generateIV() {
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
return iv;
}
CipherOutputStream
public static void encryptToFile(byte[] plaintext, File outputFile, SecretKey key) throws Exception {
byte[] iv = generateIV();
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
try (FileOutputStream fos = new FileOutputStream(outputFile);
// Write IV at the beginning of the file for later decryption
BufferedOutputStream bos = new BufferedOutputStream(fos);
CipherOutputStream cos = new CipherOutputStream(bos, cipher)) {
bos.write(iv); // prepend IV for use during decryption
cos.write(plaintext);
}
// Securely clear plaintext buffer
java.util.Arrays.fill(plaintext, (byte) 0);
}
This method:
FileOutputStream
in a CipherOutputStream
for transparent encryption.CipherInputStream
public static byte[] decryptFromFile(File inputFile, SecretKey key) throws Exception {
try (FileInputStream fis = new FileInputStream(inputFile);
BufferedInputStream bis = new BufferedInputStream(fis)) {
// Read the IV from the file header
byte[] iv = new byte[GCM_IV_LENGTH];
if (bis.read(iv) != GCM_IV_LENGTH) {
throw new IllegalStateException("Invalid input file format");
}
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
try (CipherInputStream cis = new CipherInputStream(bis, cipher);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = cis.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
byte[] decrypted = baos.toByteArray();
// Securely clear intermediate buffer
java.util.Arrays.fill(buffer, (byte) 0);
return decrypted;
}
}
}
}
This method:
CipherInputStream
to decrypt transparently.
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.io.*;
import java.security.SecureRandom;
import java.util.Arrays;
public class SecureStreamExample {
private static final String AES = "AES";
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 16; // in bytes
private static final int GCM_IV_LENGTH = 12; // in bytes
public static void main(String[] args) throws Exception {
SecretKey key = generateKey();
File file = new File("encrypted.dat");
String message = "This is a top-secret message.";
byte[] plaintext = message.getBytes();
encryptToFile(plaintext, file, key);
byte[] decrypted = decryptFromFile(file, key);
System.out.println("Decrypted message: " + new String(decrypted));
}
// Generate a 256-bit AES key
public static SecretKey generateKey() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance(AES);
keyGen.init(256);
return keyGen.generateKey();
}
// Generate secure random IV
public static byte[] generateIV() {
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
return iv;
}
// Encrypt data and write to file
public static void encryptToFile(byte[] plaintext, File outputFile, SecretKey key) throws Exception {
byte[] iv = generateIV();
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
try (FileOutputStream fos = new FileOutputStream(outputFile);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
bos.write(iv); // prepend IV
try (CipherOutputStream cos = new CipherOutputStream(bos, cipher)) {
cos.write(plaintext);
}
}
Arrays.fill(plaintext, (byte) 0); // Clear sensitive data
}
// Decrypt data from file
public static byte[] decryptFromFile(File inputFile, SecretKey key) throws Exception {
try (FileInputStream fis = new FileInputStream(inputFile);
BufferedInputStream bis = new BufferedInputStream(fis)) {
byte[] iv = new byte[GCM_IV_LENGTH];
if (bis.read(iv) != GCM_IV_LENGTH) {
throw new IllegalStateException("Invalid file format: IV missing");
}
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
try (CipherInputStream cis = new CipherInputStream(bis, cipher);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = cis.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
Arrays.fill(buffer, (byte) 0);
return baos.toByteArray();
}
}
}
}
String
for secrets: Store sensitive data in mutable char[]
or byte[]
so you can overwrite it.Handling sensitive data securely in Java IO streams involves:
javax.crypto
) to encrypt and decrypt data with CipherOutputStream
and CipherInputStream
.By following these practices and using the Java cryptographic APIs properly, you can protect sensitive data throughout its lifecycle in your Java applications.