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.util;
020
021import java.io.*;
022import java.nio.charset.StandardCharsets;
023import java.security.*;
024import java.util.*;
025import java.util.regex.Matcher;
026import java.util.regex.Pattern;
027
028import org.bouncycastle.bcpg.ArmoredInputStream;
029import org.bouncycastle.bcpg.ArmoredOutputStream;
030import org.bouncycastle.jce.provider.BouncyCastleProvider;
031import org.bouncycastle.openpgp.*;
032import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator;
033import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
034import org.bouncycastle.openpgp.operator.bc.*;
035import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
036import org.jpos.core.Environment;
037import org.jpos.iso.ISOUtil;
038import org.jpos.log.evt.License;
039import org.jpos.q2.Q2;
040import org.jpos.q2.install.ModuleUtils;
041
042import javax.crypto.Mac;
043import javax.crypto.spec.SecretKeySpec;
044
045import static org.bouncycastle.bcpg.ArmoredOutputStream.VERSION_HDR;
046
047public class PGPHelper {
048    private static KeyFingerPrintCalculator fingerPrintCalculator = new BcKeyFingerprintCalculator();
049    private static final String PUBRING = "META-INF/.pgp/pubring.asc";
050    private static final String SIGNER = "license@jpos.org";
051    private static int node;
052    static {
053        if(Security.getProvider("BC") == null)
054            Security.addProvider(new BouncyCastleProvider());
055
056        String nodeString = Environment.get("${q2.node:1}");
057        Pattern pattern = Pattern.compile("\\d+");
058
059        try {
060            Matcher matcher = pattern.matcher(nodeString);
061            node = (nodeString == null || nodeString.isEmpty())
062              ? 1 // Default value if nodeString is null or empty
063              : (matcher.find()
064              ? Integer.parseInt(matcher.group()) // Use matched digits if found
065              : 1); // Default value if no match is found
066        } catch (Throwable e) {
067            node = 0; // Fallback to default value in case of any exception
068        }
069    }
070
071    private static boolean verifySignature(InputStream in, PGPPublicKey pk) throws IOException, PGPException {
072        boolean verify = false;
073        boolean newl = false;
074        int ch;
075        ArmoredInputStream ain = new ArmoredInputStream(in, true);
076        ByteArrayOutputStream out = new ByteArrayOutputStream();
077
078        while ((ch = ain.read()) >= 0 && ain.isClearText()) {
079            if (newl) {
080                out.write((byte) '\n');
081                newl = false;
082            }
083            if (ch == '\n') {
084                newl = true;
085                continue;
086            }
087            out.write((byte) ch);
088        }
089        PGPObjectFactory pgpf = new PGPObjectFactory(ain, fingerPrintCalculator);
090        Object o = pgpf.nextObject();
091        if (o instanceof PGPSignatureList) {
092            PGPSignatureList list = (PGPSignatureList)o;
093            if (list.size() > 0) {
094                PGPSignature sig = list.get(0);
095                sig.init (new JcaPGPContentVerifierBuilderProvider().setProvider("BC"), pk);
096                while ((ch = ain.read()) >= 0 && ain.isClearText()) {
097                    if (newl) {
098                        out.write((byte) '\n');
099                        newl = false;
100                    }
101                    if (ch == '\n') {
102                        newl = true;
103                        continue;
104                    }
105                    out.write((byte) ch);
106                }
107                sig.update(out.toByteArray());
108                verify = sig.verify();
109            }
110        }
111        return verify;
112    }
113
114    private static PGPPublicKey readPublicKey(InputStream in, String id)
115            throws IOException, PGPException
116    {
117        in = PGPUtil.getDecoderStream(in);
118        id = id.toLowerCase();
119
120        PGPPublicKeyRingCollection pubRings = new PGPPublicKeyRingCollection(in, fingerPrintCalculator);
121        Iterator rIt = pubRings.getKeyRings();
122        while (rIt.hasNext()) {
123            PGPPublicKeyRing pgpPub = (PGPPublicKeyRing) rIt.next();
124            try {
125                pgpPub.getPublicKey();
126            }
127            catch (Exception ignored) {
128                continue;
129            }
130            Iterator kIt = pgpPub.getPublicKeys();
131            boolean isId = false;
132            while (kIt.hasNext()) {
133                PGPPublicKey pgpKey = (PGPPublicKey) kIt.next();
134
135                Iterator iter = pgpKey.getUserIDs();
136                while (iter.hasNext()) {
137                    String uid = (String) iter.next();
138                    if (uid.toLowerCase().contains(id)) {
139                        isId = true;
140                        break;
141                    }
142                }
143                if (pgpKey.isEncryptionKey() && isId && Arrays.equals(new byte[] {
144                  (byte) 0x59, (byte) 0xA9, (byte) 0x23, (byte) 0x24, (byte) 0xE9, (byte) 0x3B, (byte) 0x28, (byte) 0xE8,
145                  (byte) 0xA3, (byte) 0x82, (byte) 0xA0, (byte) 0x51, (byte) 0xE4, (byte) 0x32, (byte) 0x78, (byte) 0xEE,
146                  (byte) 0xF5, (byte) 0x9D, (byte) 0x8B, (byte) 0x45}, pgpKey.getFingerprint())) {
147                    return pgpKey;
148                }
149            }
150        }
151        throw new IllegalArgumentException("Can't find encryption key in key ring.");
152    }
153    public static boolean checkSignature() {
154        boolean ok = false;
155        try (InputStream is = getLicenseeStream()) {
156            InputStream ks = Q2.class.getClassLoader().getResourceAsStream(PUBRING);
157            PGPPublicKey pk = PGPHelper.readPublicKey(ks, SIGNER);
158            ok = verifySignature(is, pk);
159        } catch (Exception ignored) {
160            // NOPMD: signature isn't good
161        }
162        return ok;
163    }
164
165    public static int checkLicense() {
166        int rc = 0x90000;
167        boolean newl = false;
168        int ch;
169
170        try (InputStream in = getLicenseeStream()){
171            InputStream ks = Q2.class.getClassLoader().getResourceAsStream(PUBRING);
172            PGPPublicKey pk = readPublicKey(ks, SIGNER);
173            ArmoredInputStream ain = new ArmoredInputStream(in, true);
174            ByteArrayOutputStream out = new ByteArrayOutputStream();
175            Mac mac = Mac.getInstance("HmacSHA256");
176            mac.init(new SecretKeySpec(pk.getFingerprint(), "HmacSHA256"));
177
178            while ((ch = ain.read()) >= 0 && ain.isClearText()) {
179                if (newl) {
180                    out.write((byte) '\n');
181                    newl = false;
182                }
183                if (ch == '\n') {
184                    newl = true;
185                    continue;
186                }
187                out.write((byte) ch);
188            }
189            PGPObjectFactory pgpf = new PGPObjectFactory(ain, fingerPrintCalculator);
190            Object o = pgpf.nextObject();
191            if (o instanceof PGPSignatureList) {
192                PGPSignatureList list = (PGPSignatureList) o;
193                if (list.size() > 0) {
194                    PGPSignature sig = list.get(0);
195                    sig.init(new JcaPGPContentVerifierBuilderProvider().setProvider("BC"), pk);
196                    while ((ch = ain.read()) >= 0 && ain.isClearText()) {
197                        if (newl) {
198                            out.write((byte) '\n');
199                            newl = false;
200                        }
201                        if (ch == '\n') {
202                            newl = true;
203                            continue;
204                        }
205                        out.write((byte) ch);
206                    }
207                    sig.update(out.toByteArray());
208                    if (sig.verify()) {
209                        rc &= 0x7FFFF;
210                        ByteArrayInputStream bais = new ByteArrayInputStream(out.toByteArray());
211                        BufferedReader reader = new BufferedReader(new InputStreamReader(bais, StandardCharsets.UTF_8));
212                        String s;
213                        Pattern p1 = Pattern.compile("\\s(valid through:)\\s(\\d\\d\\d\\d-\\d\\d-\\d\\d)?", Pattern.CASE_INSENSITIVE);
214                        Pattern p2 = Pattern.compile("\\s(instances:)\\s([\\d]{0,4})?", Pattern.CASE_INSENSITIVE);
215                        String h = ModuleUtils.getSystemHash();
216                        while ((s = reader.readLine()) != null) {
217                            Matcher matcher = p1.matcher(s);
218                            if (matcher.find() && matcher.groupCount() == 2) {
219                                String lDate = matcher.group(2);
220                                if (lDate.compareTo(Q2.getBuildTimestamp().substring(0, 10)) < 0) {
221                                    rc |= 0x40000;
222                                }
223                            }
224                            matcher = p2.matcher(s);
225                            if (matcher.find() && matcher.groupCount() == 2) {
226                                int n = Integer.parseInt(matcher.group(2));
227                                node = n >= node ? node : 0;
228                                rc |= n;
229                            }
230                            if (s.contains(h)) {
231                                rc &= 0xEFFFF;
232                            }
233                        }
234                    }
235                }
236                if (!Arrays.equals(Q2.PUBKEYHASH, mac.doFinal(pk.getEncoded())))
237                    rc |= 0x20000;
238                if (ModuleUtils.getRKeys().contains(PGPHelper.getLicenseeHash()))
239                    rc |= 0x80000;
240            }
241        } catch (Exception ignored) {
242            // NOPMD: signature isn't good
243        }
244        return rc;
245    }
246
247    static InputStream getLicenseeStream() throws FileNotFoundException {
248        String lf = System.getProperty("LICENSEE");
249        File l = new File (lf != null ? lf : Q2.LICENSEE);
250        return l.canRead() && l.length() < 8192 ? new FileInputStream(l) : Q2.class.getClassLoader().getResourceAsStream(Q2.LICENSEE);
251    }
252    public static String getLicensee() throws IOException {
253        ByteArrayOutputStream baos = new ByteArrayOutputStream();
254        try (InputStream is = getLicenseeStream()) {
255            if (is != null) {
256                try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
257                    PrintStream p = new PrintStream(baos, false, StandardCharsets.UTF_8.name());
258                    p.println();
259                    p.println();
260                    while(br.ready())
261                      p.println(br.readLine());
262                }
263            }
264        }
265        return baos.toString(StandardCharsets.UTF_8.name());
266    }
267    public static String getLicenseeHash() throws IOException, NoSuchAlgorithmException {
268        return ISOUtil.hexString(hash(getLicensee()));
269    }
270
271    public static int node () {
272        return node;
273    }
274
275    /**
276     * Simple PGP encryptor between byte[].
277     *
278     * @param clearData The test to be encrypted
279     * @param keyRing public key ring input stream
280     * @param fileName  File name. This is used in the Literal Data Packet (tag 11)
281     *                  which is really only important if the data is to be related to
282     *                  a file to be recovered later. Because this routine does not
283     *                  know the source of the information, the caller can set
284     *                  something here for file name use that will be carried. If this
285     *                  routine is being used to encrypt SOAP MIME bodies, for
286     *                  example, use the file name from the MIME type, if applicable.
287     *                  Or anything else appropriate.
288     * @param withIntegrityCheck true if an integrity packet is to be included
289     * @param armor true for ascii armor
290     * @param ids destination ids
291     * @return encrypted data.
292     * @throws IOException
293     * @throws PGPException
294     * @throws NoSuchProviderException
295     * @throws NoSuchAlgorithmException
296     */
297    public static byte[] encrypt(byte[] clearData, InputStream keyRing,
298                                 String fileName, boolean withIntegrityCheck,
299                                 boolean armor, String... ids)
300      throws IOException, PGPException, NoSuchProviderException, NoSuchAlgorithmException {
301        if (fileName == null) {
302            fileName = PGPLiteralData.CONSOLE;
303        }
304        PGPPublicKey[] encKeys = readPublicKeys(keyRing, ids);
305        ByteArrayOutputStream encOut = new ByteArrayOutputStream();
306        OutputStream out = encOut;
307        if (armor) {
308            out = ArmoredOutputStream.builder()
309              .setVersion("BCPG/jPOS " + Q2.getVersion())
310              .build(out);
311        }
312        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
313
314        PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(
315          PGPCompressedDataGenerator.ZIP);
316        OutputStream cos = comData.open(bOut); // compressed output stream
317        PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator();
318        OutputStream pOut = lData.open(cos,
319          PGPLiteralData.BINARY, fileName,
320          clearData.length,
321          new Date()
322        );
323        pOut.write(clearData);
324
325        lData.close();
326        comData.close();
327        BcPGPDataEncryptorBuilder dataEncryptor =
328          new BcPGPDataEncryptorBuilder(PGPEncryptedData.TRIPLE_DES);
329        dataEncryptor.setWithIntegrityPacket(withIntegrityCheck);
330        dataEncryptor.setSecureRandom(new SecureRandom());
331
332        PGPEncryptedDataGenerator cPk = new PGPEncryptedDataGenerator(dataEncryptor);
333        for (PGPPublicKey pk : encKeys)
334            cPk.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(pk));
335
336        byte[] bytes = bOut.toByteArray();
337        OutputStream cOut = cPk.open(out, bytes.length);
338        cOut.write(bytes);
339        cOut.close();
340        out.close();
341        return encOut.toByteArray();
342    }
343
344
345    /**
346     * Simple PGP encryptor between byte[].
347     *
348     * @param clearData The test to be encrypted
349     * @param keyRing public key ring input stream
350     * @param fileName  File name. This is used in the Literal Data Packet (tag 11)
351     *                  which is really only important if the data is to be related to
352     *                  a file to be recovered later. Because this routine does not
353     *                  know the source of the information, the caller can set
354     *                  something here for file name use that will be carried. If this
355     *                  routine is being used to encrypt SOAP MIME bodies, for
356     *                  example, use the file name from the MIME type, if applicable.
357     *                  Or anything else appropriate.
358     * @param withIntegrityCheck true if an integrity packet is to be included
359     * @param armor true for ascii armor
360     * @param ids destination ids
361     * @return encrypted data.
362     * @throws IOException
363     * @throws PGPException
364     * @throws NoSuchProviderException
365     * @throws NoSuchAlgorithmException
366     */
367    public static byte[] encrypt(byte[] clearData, String keyRing,
368                                 String fileName, boolean withIntegrityCheck,
369                                 boolean armor, String... ids)
370      throws IOException, PGPException, NoSuchProviderException, NoSuchAlgorithmException {
371        return encrypt (clearData, new FileInputStream(keyRing), fileName, withIntegrityCheck, armor, ids);
372    }
373
374    /**
375     * decrypt the passed in message stream
376     *
377     * @param encrypted The message to be decrypted.
378     * @param password  Pass phrase (key)
379     * @return Clear text as a byte array. I18N considerations are not handled
380     *         by this routine
381     * @throws IOException
382     * @throws PGPException
383     * @throws NoSuchProviderException
384     */
385    public static byte[] decrypt(byte[] encrypted, InputStream keyIn, char[] password)
386      throws IOException, PGPException, NoSuchProviderException {
387        InputStream in = PGPUtil.getDecoderStream(new ByteArrayInputStream(encrypted));
388        PGPObjectFactory pgpF = new PGPObjectFactory(in, fingerPrintCalculator);
389        PGPEncryptedDataList enc;
390        Object o = pgpF.nextObject();
391
392        //
393        // the first object might be a PGP marker packet.
394        //
395        if (o instanceof PGPEncryptedDataList) {
396            enc = (PGPEncryptedDataList) o;
397        } else {
398            enc = (PGPEncryptedDataList) pgpF.nextObject();
399        }
400
401        //
402        // find the secret key
403        //
404        Iterator it = enc.getEncryptedDataObjects();
405        PGPPrivateKey sKey = null;
406        PGPPublicKeyEncryptedData pbe = null;
407        PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
408          PGPUtil.getDecoderStream(keyIn), fingerPrintCalculator);
409
410        while (sKey == null && it.hasNext()) {
411            pbe = (PGPPublicKeyEncryptedData) it.next();
412            sKey = findSecretKey(pgpSec, pbe.getKeyID(), password);
413        }
414
415        if (sKey == null) {
416            throw new IllegalArgumentException(
417              "secret key for message not found.");
418        }
419
420        InputStream clear = pbe.getDataStream(new BcPublicKeyDataDecryptorFactory(sKey));
421        PGPObjectFactory pgpFact = new PGPObjectFactory(clear, fingerPrintCalculator);
422        PGPCompressedData cData = (PGPCompressedData) pgpFact.nextObject();
423        pgpFact = new PGPObjectFactory(cData.getDataStream(), fingerPrintCalculator);
424        PGPLiteralData ld = (PGPLiteralData) pgpFact.nextObject();
425        InputStream unc = ld.getInputStream();
426        ByteArrayOutputStream out = new ByteArrayOutputStream();
427        int ch;
428
429        while ((ch = unc.read()) >= 0) {
430            out.write(ch);
431        }
432        byte[] returnBytes = out.toByteArray();
433        out.close();
434        return returnBytes;
435    }
436
437    /**
438     * decrypt the passed in message stream
439     *
440     * @param encrypted The message to be decrypted.
441     * @param password  Pass phrase (key)
442     * @return Clear text as a byte array. I18N considerations are not handled
443     *         by this routine
444     * @throws IOException
445     * @throws PGPException
446     * @throws NoSuchProviderException
447     */
448    public static byte[] decrypt(byte[] encrypted, String keyIn, char[] password)
449      throws IOException, PGPException, NoSuchProviderException {
450        return decrypt (encrypted, new FileInputStream(keyIn), password);
451    }
452
453    public static License getLicense() throws IOException {
454        return new License(getLicensee(), checkLicense());
455    }
456
457    private static PGPPublicKey[] readPublicKeys(InputStream in, String[] ids)
458      throws IOException, PGPException
459    {
460        in = PGPUtil.getDecoderStream(in);
461        List<PGPPublicKey> keys = new ArrayList<>();
462
463        PGPPublicKeyRingCollection pubRings = new PGPPublicKeyRingCollection(in, fingerPrintCalculator);
464        Iterator rIt = pubRings.getKeyRings();
465        while (rIt.hasNext()) {
466            PGPPublicKeyRing pgpPub = (PGPPublicKeyRing) rIt.next();
467            try {
468                pgpPub.getPublicKey();
469            }
470            catch (Exception e) {
471                e.printStackTrace();
472                continue;
473            }
474            Iterator kIt = pgpPub.getPublicKeys();
475            boolean isId = false;
476            while (kIt.hasNext()) {
477                PGPPublicKey pgpKey = (PGPPublicKey) kIt.next();
478
479                Iterator iter = pgpKey.getUserIDs();
480                while (iter.hasNext()) {
481                    String uid = (String) iter.next();
482                    // System.out.println("    uid: " + uid + " isEncryption? "+ pgpKey.isEncryptionKey());
483                    for (String id : ids) {
484                        if (uid.toLowerCase().indexOf(id.toLowerCase()) >= 0) {
485                            isId = true;
486                        }
487                    }
488                }
489                if (isId && pgpKey.isEncryptionKey()) {
490                    keys.add(pgpKey);
491                    isId = false;
492                }
493            }
494        }
495        if (keys.size() == 0)
496            throw new IllegalArgumentException("Can't find encryption key in key ring.");
497
498        return keys.toArray(new PGPPublicKey[keys.size()]);
499    }
500
501    private static PGPPrivateKey findSecretKey(
502      PGPSecretKeyRingCollection pgpSec, long keyID, char[] pass)
503      throws PGPException, NoSuchProviderException {
504        PGPSecretKey pgpSecKey = pgpSec.getSecretKey(keyID);
505
506        if (pgpSecKey == null) {
507            return null;
508        }
509        PBESecretKeyDecryptor decryptor = new BcPBESecretKeyDecryptorBuilder(
510          new BcPGPDigestCalculatorProvider()
511        ).build(pass);
512
513        return pgpSecKey.extractPrivateKey(decryptor);
514    }
515
516    private static byte[] hash (String s) throws NoSuchAlgorithmException {
517        MessageDigest md = MessageDigest.getInstance("SHA-1");
518        return md.digest(s.getBytes(StandardCharsets.UTF_8));
519    }
520}