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}