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