001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.security; 020 021import org.bouncycastle.jce.provider.BouncyCastleProvider; 022import org.jpos.iso.ISOUtil; 023 024import javax.crypto.*; 025import javax.crypto.spec.IvParameterSpec; 026import java.lang.ref.Cleaner; 027import java.nio.ByteBuffer; 028import java.security.*; 029import java.util.Arrays; 030import java.util.Random; 031import java.util.function.Supplier; 032 033/** 034 * Holds a sensitive string in memory under AES-GCM encryption with a 035 * per-instance key, exposing the cleartext only on demand and zeroing 036 * the buffer when the instance is cleaned. 037 */ 038public class SensitiveString implements Supplier<String>, AutoCloseable { 039 private SecretKey key; 040 private byte[] encoded; 041 private static Random rnd; 042 private static final String AES = "AES/GCM/NoPadding"; 043 044 private static final Cleaner cleaner = Cleaner.create(); 045 private Cleaner.Cleanable cleanable; 046 047 static { 048 rnd = new SecureRandom(); 049 if(Security.getProvider("BC") == null) 050 Security.addProvider(new BouncyCastleProvider()); 051 } 052 053 /** 054 * Constructs a new SensitiveString holding the encrypted form of {@code s}. 055 * 056 * @param s plaintext to protect 057 * @throws NoSuchAlgorithmException if AES is unavailable 058 * @throws NoSuchPaddingException if the configured padding is unavailable 059 * @throws InvalidKeyException if the generated key is rejected 060 * @throws IllegalBlockSizeException on AES-GCM block-size errors 061 * @throws BadPaddingException on AES-GCM padding errors 062 * @throws NoSuchProviderException if the configured provider is unavailable 063 * @throws InvalidAlgorithmParameterException if the IV/parameters are rejected 064 */ 065 public SensitiveString(String s) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchProviderException, InvalidAlgorithmParameterException { 066 key = generateKey(); 067 encoded = encrypt(s.getBytes()); 068 cleanable = cleaner.register(this, this::clean); 069 } 070 071 @Override 072 public boolean equals(Object o) { 073 if (this == o) return true; 074 if (o == null || getClass() != o.getClass()) return false; 075 SensitiveString that = (SensitiveString) o; 076 return this.get().equals(that.get()); 077 } 078 079 private SecretKey generateKey() throws NoSuchAlgorithmException { 080 KeyGenerator keyGen = KeyGenerator.getInstance("AES"); 081 int maxKeyLength = Cipher.getMaxAllowedKeyLength(keyGen.getAlgorithm()); 082 keyGen.init(maxKeyLength == Integer.MAX_VALUE ? 256 : maxKeyLength); 083 return keyGen.generateKey(); 084 } 085 086 private byte[] encrypt(byte[] b) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { 087 final Cipher cipher = Cipher.getInstance(AES); 088 final byte[] iv = randomIV(); 089 cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv)); 090 byte[] enc = cipher.doFinal(b); 091 ByteBuffer buf = ByteBuffer.allocate(iv.length + enc.length); 092 buf.put(ISOUtil.xor(iv, SystemSeed.getSeed(iv.length, iv.length))); 093 buf.put(enc); 094 return buf.array(); 095 } 096 private byte[] decrypt(byte[] encoded) 097 throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, 098 IllegalBlockSizeException, NoSuchProviderException, InvalidAlgorithmParameterException 099 { 100 byte[] iv = new byte[16]; 101 byte[] cryptogram = new byte[encoded.length - iv.length]; 102 System.arraycopy(encoded, 0, iv, 0, iv.length); 103 System.arraycopy(encoded, iv.length, cryptogram, 0, cryptogram.length); 104 final Cipher cipher = Cipher.getInstance(AES); 105 cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ISOUtil.xor(iv, SystemSeed.getSeed(iv.length, iv.length)))); 106 return cipher.doFinal(cryptogram); 107 } 108 109 private byte[] randomIV() { 110 final byte[] b = new byte[16]; 111 rnd.nextBytes(b); 112 return b; 113 } 114 115 /** Zeroes the encrypted buffer so the cleartext is no longer recoverable. */ 116 public void clean () { 117 byte[] b = encoded; 118 encoded = null; 119 Arrays.fill (b, (byte) 0); 120 } 121 122 @Override 123 public String get() { 124 if (encoded == null) 125 throw new IllegalStateException ("SensitiveString not available"); 126 try { 127 return new String(decrypt(encoded)); 128 } catch (NoSuchPaddingException | NoSuchAlgorithmException | BadPaddingException | InvalidKeyException | NoSuchProviderException | IllegalBlockSizeException | InvalidAlgorithmParameterException e) { 129 throw new AssertionError(e.getMessage()); 130 } 131 } 132 133 @Override 134 public void close() throws Exception { 135 clean(); 136 } 137}