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
047/**
048 * PGP utility helpers used by jPOS for license verification, public-key
049 * loading, and simple encryption/decryption with Bouncy Castle.
050 */
051public class PGPHelper {
052    /** Utility class; instances carry no state. */
053    public PGPHelper() {}
054    private static KeyFingerPrintCalculator fingerPrintCalculator = new BcKeyFingerprintCalculator();
055    private static final String PUBRING = "META-INF/.pgp/pubring.asc";
056    private static final String SIGNER = "license@jpos.org";
057    private static int node;
058    static {
059        if(Security.getProvider("BC") == null)
060            Security.addProvider(new BouncyCastleProvider());
061
062        String nodeString = Environment.get("${q2.node:1}");
063        Pattern pattern = Pattern.compile("\\d+");
064
065        try {
066            Matcher matcher = pattern.matcher(nodeString);
067            node = (nodeString == null || nodeString.isEmpty())
068              ? 1 // Default value if nodeString is null or empty
069              : (matcher.find()
070              ? Integer.parseInt(matcher.group()) // Use matched digits if found
071              : 1); // Default value if no match is found
072        } catch (Throwable e) {
073            node = 0; // Fallback to default value in case of any exception
074        }
075    }
076
077    private static boolean verifySignature(InputStream in, PGPPublicKey pk) throws IOException, PGPException {
078        boolean verify = false;
079        boolean newl = false;
080        int ch;
081        ArmoredInputStream ain = new ArmoredInputStream(in, true);
082        ByteArrayOutputStream out = new ByteArrayOutputStream();
083
084        while ((ch = ain.read()) >= 0 && ain.isClearText()) {
085            if (newl) {
086                out.write((byte) '\n');
087                newl = false;
088            }
089            if (ch == '\n') {
090                newl = true;
091                continue;
092            }
093            out.write((byte) ch);
094        }
095        PGPObjectFactory pgpf = new PGPObjectFactory(ain, fingerPrintCalculator);
096        Object o = pgpf.nextObject();
097        if (o instanceof PGPSignatureList) {
098            PGPSignatureList list = (PGPSignatureList)o;
099            if (list.size() > 0) {
100                PGPSignature sig = list.get(0);
101                sig.init (new JcaPGPContentVerifierBuilderProvider().setProvider("BC"), pk);
102                while ((ch = ain.read()) >= 0 && ain.isClearText()) {
103                    if (newl) {
104                        out.write((byte) '\n');
105                        newl = false;
106                    }
107                    if (ch == '\n') {
108                        newl = true;
109                        continue;
110                    }
111                    out.write((byte) ch);
112                }
113                sig.update(out.toByteArray());
114                verify = sig.verify();
115            }
116        }
117        return verify;
118    }
119
120    private static String readClearText(InputStream in) throws IOException {
121        boolean newl = false;
122        int ch;
123        ArmoredInputStream ain = new ArmoredInputStream(in, true);
124        ByteArrayOutputStream out = new ByteArrayOutputStream();
125
126        while ((ch = ain.read()) >= 0 && ain.isClearText()) {
127            if (newl) {
128                out.write((byte) '\n');
129                newl = false;
130            }
131            if (ch == '\n') {
132                newl = true;
133                continue;
134            }
135            out.write((byte) ch);
136        }
137        return out.toString(StandardCharsets.UTF_8.name());
138    }
139
140    private static PGPPublicKey readPublicKey(InputStream in, String id)
141            throws IOException, PGPException
142    {
143        in = PGPUtil.getDecoderStream(in);
144        id = id.toLowerCase();
145
146        PGPPublicKeyRingCollection pubRings = new PGPPublicKeyRingCollection(in, fingerPrintCalculator);
147        Iterator rIt = pubRings.getKeyRings();
148        while (rIt.hasNext()) {
149            PGPPublicKeyRing pgpPub = (PGPPublicKeyRing) rIt.next();
150            try {
151                pgpPub.getPublicKey();
152            }
153            catch (Exception ignored) {
154                continue;
155            }
156            Iterator kIt = pgpPub.getPublicKeys();
157            boolean isId = false;
158            while (kIt.hasNext()) {
159                PGPPublicKey pgpKey = (PGPPublicKey) kIt.next();
160
161                Iterator iter = pgpKey.getUserIDs();
162                while (iter.hasNext()) {
163                    String uid = (String) iter.next();
164                    if (uid.toLowerCase().contains(id)) {
165                        isId = true;
166                        break;
167                    }
168                }
169                if (pgpKey.isEncryptionKey() && isId && Arrays.equals(new byte[] {
170                  (byte) 0x59, (byte) 0xA9, (byte) 0x23, (byte) 0x24, (byte) 0xE9, (byte) 0x3B, (byte) 0x28, (byte) 0xE8,
171                  (byte) 0xA3, (byte) 0x82, (byte) 0xA0, (byte) 0x51, (byte) 0xE4, (byte) 0x32, (byte) 0x78, (byte) 0xEE,
172                  (byte) 0xF5, (byte) 0x9D, (byte) 0x8B, (byte) 0x45}, pgpKey.getFingerprint())) {
173                    return pgpKey;
174                }
175            }
176        }
177        throw new IllegalArgumentException("Can't find encryption key in key ring.");
178    }
179    /**
180     * Verifies the signature on the bundled licensee file using the embedded jPOS public key.
181     *
182     * @return {@code true} if the signature verifies, {@code false} otherwise (including any error)
183     */
184    public static boolean checkSignature() {
185        boolean ok = false;
186        try (InputStream is = getLicenseeStream()) {
187            InputStream ks = Q2.class.getClassLoader().getResourceAsStream(PUBRING);
188            PGPPublicKey pk = PGPHelper.readPublicKey(ks, SIGNER);
189            ok = verifySignature(is, pk);
190        } catch (Exception ignored) {
191            // NOPMD: signature isn't good
192        }
193        return ok;
194    }
195
196    /**
197     * Verifies the licensee file's signature, parses its metadata, and returns
198     * a packed status code combining expiration, fingerprint match, instance
199     * count, and revocation flags.
200     *
201     * @return packed status code; bits encode validity, expiration, fingerprint
202     *         match, revocation, and the configured instance count
203     */
204    public static int checkLicense() {
205        try (InputStream in = getLicenseeStream()){
206            return checkLicense(in);
207        } catch (Exception ignored) {
208            // NOPMD: signature isn't good
209        }
210        return 0x90000;
211    }
212
213    private static int checkLicense(InputStream in) {
214        int rc = 0x90000;
215        boolean newl = false;
216        int ch;
217
218        try {
219            InputStream ks = Q2.class.getClassLoader().getResourceAsStream(PUBRING);
220            PGPPublicKey pk = readPublicKey(ks, SIGNER);
221            ArmoredInputStream ain = new ArmoredInputStream(in, true);
222            ByteArrayOutputStream out = new ByteArrayOutputStream();
223            Mac mac = Mac.getInstance("HmacSHA256");
224            mac.init(new SecretKeySpec(pk.getFingerprint(), "HmacSHA256"));
225
226            while ((ch = ain.read()) >= 0 && ain.isClearText()) {
227                if (newl) {
228                    out.write((byte) '\n');
229                    newl = false;
230                }
231                if (ch == '\n') {
232                    newl = true;
233                    continue;
234                }
235                out.write((byte) ch);
236            }
237            PGPObjectFactory pgpf = new PGPObjectFactory(ain, fingerPrintCalculator);
238            Object o = pgpf.nextObject();
239            if (o instanceof PGPSignatureList) {
240                PGPSignatureList list = (PGPSignatureList) o;
241                if (list.size() > 0) {
242                    PGPSignature sig = list.get(0);
243                    sig.init(new JcaPGPContentVerifierBuilderProvider().setProvider("BC"), pk);
244                    while ((ch = ain.read()) >= 0 && ain.isClearText()) {
245                        if (newl) {
246                            out.write((byte) '\n');
247                            newl = false;
248                        }
249                        if (ch == '\n') {
250                            newl = true;
251                            continue;
252                        }
253                        out.write((byte) ch);
254                    }
255                    sig.update(out.toByteArray());
256                    if (sig.verify()) {
257                        rc &= 0x7FFFF;
258                        ByteArrayInputStream bais = new ByteArrayInputStream(out.toByteArray());
259                        BufferedReader reader = new BufferedReader(new InputStreamReader(bais, StandardCharsets.UTF_8));
260                        String s;
261                        Pattern p1 = Pattern.compile("\\s(valid through:)\\s(\\d\\d\\d\\d-\\d\\d-\\d\\d)?", Pattern.CASE_INSENSITIVE);
262                        Pattern p2 = Pattern.compile("\\s(instances:)\\s([\\d]{0,4})?", Pattern.CASE_INSENSITIVE);
263                        String h = ModuleUtils.getSystemHash();
264                        while ((s = reader.readLine()) != null) {
265                            Matcher matcher = p1.matcher(s);
266                            if (matcher.find() && matcher.groupCount() == 2) {
267                                String lDate = matcher.group(2);
268                                if (lDate.compareTo(Q2.getBuildTimestamp().substring(0, 10)) < 0) {
269                                    rc |= 0x40000;
270                                }
271                            }
272                            matcher = p2.matcher(s);
273                            if (matcher.find() && matcher.groupCount() == 2) {
274                                int n = Integer.parseInt(matcher.group(2));
275                                node = n >= node ? node : 0;
276                                rc |= n;
277                            }
278                            if (s.contains(h)) {
279                                rc &= 0xEFFFF;
280                            }
281                        }
282                    }
283                }
284                if (!Arrays.equals(Q2.PUBKEYHASH, mac.doFinal(pk.getEncoded())))
285                    rc |= 0x20000;
286                if (ModuleUtils.getRKeys().contains(PGPHelper.getLicenseeHash()))
287                    rc |= 0x80000;
288            }
289        } catch (Exception ignored) {
290            // NOPMD: signature isn't good
291        }
292        return rc;
293    }
294
295    /**
296     * Returns the verified clear-text license payload.
297     * <p>
298     * The returned value is the text covered by the clear-text PGP signature, not
299     * the armored license block. If the bundled or configured license cannot be
300     * signature-verified, or if {@link #checkLicense()} reports an unacceptable
301     * status, this method returns {@code null}.
302     * <p>
303     * Status bit {@code 0x10000} (license not bound to this system hash, used by
304     * the Community Edition license) is considered acceptable. Critical status
305     * bits {@code 0xE0000} are not.
306     *
307     * @return verified clear-text license payload, or {@code null}
308     * @throws IOException if the license stream cannot be read
309     */
310    public static String getVerifiedLicenseText() throws IOException {
311        byte[] license;
312        try (InputStream is = getLicenseeStream()) {
313            if (is == null)
314                return null;
315            license = is.readAllBytes();
316        }
317        try (InputStream ks = Q2.class.getClassLoader().getResourceAsStream(PUBRING)) {
318            PGPPublicKey pk = readPublicKey(ks, SIGNER);
319            if (!verifySignature(new ByteArrayInputStream(license), pk))
320                return null;
321        } catch (PGPException | RuntimeException e) {
322            return null;
323        }
324        if ((checkLicense(new ByteArrayInputStream(license)) & 0xE0000) != 0)
325            return null;
326        return readClearText(new ByteArrayInputStream(license));
327    }
328
329    static InputStream getLicenseeStream() throws FileNotFoundException {
330        String lf = System.getProperty("LICENSEE");
331        File l = new File (lf != null ? lf : Q2.LICENSEE);
332        return l.canRead() && l.length() < 8192 ? new FileInputStream(l) : Q2.class.getClassLoader().getResourceAsStream(Q2.LICENSEE);
333    }
334    /**
335     * Returns the licensee file contents as a UTF-8 string with two leading blank lines.
336     *
337     * @return the licensee text, or empty if the licensee resource is unavailable
338     * @throws IOException if reading the licensee stream fails
339     */
340    public static String getLicensee() throws IOException {
341        ByteArrayOutputStream baos = new ByteArrayOutputStream();
342        try (InputStream is = getLicenseeStream()) {
343            if (is != null) {
344                try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
345                    PrintStream p = new PrintStream(baos, false, StandardCharsets.UTF_8.name());
346                    p.println();
347                    p.println();
348                    while(br.ready())
349                      p.println(br.readLine());
350                }
351            }
352        }
353        return baos.toString(StandardCharsets.UTF_8.name());
354    }
355    /**
356     * Returns the SHA hex hash of the licensee text as produced by {@link #getLicensee()}.
357     *
358     * @return the hex-encoded hash
359     * @throws IOException if the licensee stream cannot be read
360     * @throws NoSuchAlgorithmException if the configured digest is not available
361     */
362    public static String getLicenseeHash() throws IOException, NoSuchAlgorithmException {
363        return ISOUtil.hexString(hash(getLicensee()));
364    }
365
366    /**
367     * Returns the resolved Q2 node number used during license validation.
368     *
369     * @return the Q2 node number, or 0 if it could not be resolved
370     */
371    public static int node () {
372        return node;
373    }
374
375    /**
376     * Simple PGP encryptor between byte[].
377     *
378     * @param clearData The test to be encrypted
379     * @param keyRing public key ring input stream
380     * @param fileName  File name. This is used in the Literal Data Packet (tag 11)
381     *                  which is really only important if the data is to be related to
382     *                  a file to be recovered later. Because this routine does not
383     *                  know the source of the information, the caller can set
384     *                  something here for file name use that will be carried. If this
385     *                  routine is being used to encrypt SOAP MIME bodies, for
386     *                  example, use the file name from the MIME type, if applicable.
387     *                  Or anything else appropriate.
388     * @param withIntegrityCheck true if an integrity packet is to be included
389     * @param armor true for ascii armor
390     * @param ids destination ids
391     * @return encrypted data.
392     * @throws IOException if reading {@code keyRing} or writing the encrypted output fails
393     * @throws PGPException if a PGP-level error occurs while building the message
394     * @throws NoSuchProviderException if the {@code BC} provider is not registered
395     * @throws NoSuchAlgorithmException if the requested cipher algorithm is unavailable
396     */
397    public static byte[] encrypt(byte[] clearData, InputStream keyRing,
398                                 String fileName, boolean withIntegrityCheck,
399                                 boolean armor, String... ids)
400      throws IOException, PGPException, NoSuchProviderException, NoSuchAlgorithmException {
401        if (fileName == null) {
402            fileName = PGPLiteralData.CONSOLE;
403        }
404        PGPPublicKey[] encKeys = readPublicKeys(keyRing, ids);
405        ByteArrayOutputStream encOut = new ByteArrayOutputStream();
406        OutputStream out = encOut;
407        if (armor) {
408            out = ArmoredOutputStream.builder()
409              .setVersion("BCPG/jPOS " + Q2.getVersion())
410              .build(out);
411        }
412        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
413
414        PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(
415          PGPCompressedDataGenerator.ZIP);
416        OutputStream cos = comData.open(bOut); // compressed output stream
417        PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator();
418        OutputStream pOut = lData.open(cos,
419          PGPLiteralData.BINARY, fileName,
420          clearData.length,
421          new Date()
422        );
423        pOut.write(clearData);
424
425        lData.close();
426        comData.close();
427        BcPGPDataEncryptorBuilder dataEncryptor =
428          new BcPGPDataEncryptorBuilder(PGPEncryptedData.TRIPLE_DES);
429        dataEncryptor.setWithIntegrityPacket(withIntegrityCheck);
430        dataEncryptor.setSecureRandom(new SecureRandom());
431
432        PGPEncryptedDataGenerator cPk = new PGPEncryptedDataGenerator(dataEncryptor);
433        for (PGPPublicKey pk : encKeys)
434            cPk.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(pk));
435
436        byte[] bytes = bOut.toByteArray();
437        OutputStream cOut = cPk.open(out, bytes.length);
438        cOut.write(bytes);
439        cOut.close();
440        out.close();
441        return encOut.toByteArray();
442    }
443
444
445    /**
446     * Simple PGP encryptor between byte[].
447     *
448     * @param clearData The test to be encrypted
449     * @param keyRing public key ring input stream
450     * @param fileName  File name. This is used in the Literal Data Packet (tag 11)
451     *                  which is really only important if the data is to be related to
452     *                  a file to be recovered later. Because this routine does not
453     *                  know the source of the information, the caller can set
454     *                  something here for file name use that will be carried. If this
455     *                  routine is being used to encrypt SOAP MIME bodies, for
456     *                  example, use the file name from the MIME type, if applicable.
457     *                  Or anything else appropriate.
458     * @param withIntegrityCheck true if an integrity packet is to be included
459     * @param armor true for ascii armor
460     * @param ids destination ids
461     * @return encrypted data.
462     * @throws IOException if {@code keyRing} cannot be opened or the encrypted output cannot be written
463     * @throws PGPException if a PGP-level error occurs while building the message
464     * @throws NoSuchProviderException if the {@code BC} provider is not registered
465     * @throws NoSuchAlgorithmException if the requested cipher algorithm is unavailable
466     */
467    public static byte[] encrypt(byte[] clearData, String keyRing,
468                                 String fileName, boolean withIntegrityCheck,
469                                 boolean armor, String... ids)
470      throws IOException, PGPException, NoSuchProviderException, NoSuchAlgorithmException {
471        return encrypt (clearData, new FileInputStream(keyRing), fileName, withIntegrityCheck, armor, ids);
472    }
473
474    /**
475     * decrypt the passed in message stream
476     *
477     * @param encrypted The message to be decrypted.
478     * @param keyIn secret key ring input stream
479     * @param password  Pass phrase (key)
480     * @return Clear text as a byte array. I18N considerations are not handled
481     *         by this routine
482     * @throws IOException if {@code keyIn} or the encrypted payload cannot be read
483     * @throws PGPException if a PGP-level error occurs while decrypting
484     * @throws NoSuchProviderException if the {@code BC} provider is not registered
485     */
486    public static byte[] decrypt(byte[] encrypted, InputStream keyIn, char[] password)
487      throws IOException, PGPException, NoSuchProviderException {
488        InputStream in = PGPUtil.getDecoderStream(new ByteArrayInputStream(encrypted));
489        PGPObjectFactory pgpF = new PGPObjectFactory(in, fingerPrintCalculator);
490        PGPEncryptedDataList enc;
491        Object o = pgpF.nextObject();
492
493        //
494        // the first object might be a PGP marker packet.
495        //
496        if (o instanceof PGPEncryptedDataList) {
497            enc = (PGPEncryptedDataList) o;
498        } else {
499            enc = (PGPEncryptedDataList) pgpF.nextObject();
500        }
501
502        //
503        // find the secret key
504        //
505        Iterator it = enc.getEncryptedDataObjects();
506        PGPPrivateKey sKey = null;
507        PGPPublicKeyEncryptedData pbe = null;
508        PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
509          PGPUtil.getDecoderStream(keyIn), fingerPrintCalculator);
510
511        while (sKey == null && it.hasNext()) {
512            pbe = (PGPPublicKeyEncryptedData) it.next();
513            sKey = findSecretKey(pgpSec, pbe.getKeyID(), password);
514        }
515
516        if (sKey == null) {
517            throw new IllegalArgumentException(
518              "secret key for message not found.");
519        }
520
521        InputStream clear = pbe.getDataStream(new BcPublicKeyDataDecryptorFactory(sKey));
522        PGPObjectFactory pgpFact = new PGPObjectFactory(clear, fingerPrintCalculator);
523        PGPCompressedData cData = (PGPCompressedData) pgpFact.nextObject();
524        pgpFact = new PGPObjectFactory(cData.getDataStream(), fingerPrintCalculator);
525        PGPLiteralData ld = (PGPLiteralData) pgpFact.nextObject();
526        InputStream unc = ld.getInputStream();
527        ByteArrayOutputStream out = new ByteArrayOutputStream();
528        int ch;
529
530        while ((ch = unc.read()) >= 0) {
531            out.write(ch);
532        }
533        byte[] returnBytes = out.toByteArray();
534        out.close();
535        return returnBytes;
536    }
537
538    /**
539     * decrypt the passed in message stream
540     *
541     * @param encrypted The message to be decrypted.
542     * @param keyIn path to the secret key ring file
543     * @param password  Pass phrase (key)
544     * @return Clear text as a byte array. I18N considerations are not handled
545     *         by this routine
546     * @throws IOException if the key file or encrypted payload cannot be read
547     * @throws PGPException if a PGP-level error occurs while decrypting
548     * @throws NoSuchProviderException if the {@code BC} provider is not registered
549     */
550    public static byte[] decrypt(byte[] encrypted, String keyIn, char[] password)
551      throws IOException, PGPException, NoSuchProviderException {
552        return decrypt (encrypted, new FileInputStream(keyIn), password);
553    }
554
555    /**
556     * Returns the parsed jPOS {@link License} extracted from the licensee resource.
557     *
558     * @return the current license, including text and status flags
559     * @throws IOException if the licensee stream cannot be read
560     */
561    public static License getLicense() throws IOException {
562        return new License(getLicensee(), checkLicense());
563    }
564
565    private static PGPPublicKey[] readPublicKeys(InputStream in, String[] ids)
566      throws IOException, PGPException
567    {
568        in = PGPUtil.getDecoderStream(in);
569        List<PGPPublicKey> keys = new ArrayList<>();
570
571        PGPPublicKeyRingCollection pubRings = new PGPPublicKeyRingCollection(in, fingerPrintCalculator);
572        Iterator rIt = pubRings.getKeyRings();
573        while (rIt.hasNext()) {
574            PGPPublicKeyRing pgpPub = (PGPPublicKeyRing) rIt.next();
575            try {
576                pgpPub.getPublicKey();
577            }
578            catch (Exception e) {
579                e.printStackTrace();
580                continue;
581            }
582            Iterator kIt = pgpPub.getPublicKeys();
583            boolean isId = false;
584            while (kIt.hasNext()) {
585                PGPPublicKey pgpKey = (PGPPublicKey) kIt.next();
586
587                Iterator iter = pgpKey.getUserIDs();
588                while (iter.hasNext()) {
589                    String uid = (String) iter.next();
590                    // System.out.println("    uid: " + uid + " isEncryption? "+ pgpKey.isEncryptionKey());
591                    for (String id : ids) {
592                        if (uid.toLowerCase().indexOf(id.toLowerCase()) >= 0) {
593                            isId = true;
594                        }
595                    }
596                }
597                if (isId && pgpKey.isEncryptionKey()) {
598                    keys.add(pgpKey);
599                    isId = false;
600                }
601            }
602        }
603        if (keys.size() == 0)
604            throw new IllegalArgumentException("Can't find encryption key in key ring.");
605
606        return keys.toArray(new PGPPublicKey[keys.size()]);
607    }
608
609    private static PGPPrivateKey findSecretKey(
610      PGPSecretKeyRingCollection pgpSec, long keyID, char[] pass)
611      throws PGPException, NoSuchProviderException {
612        PGPSecretKey pgpSecKey = pgpSec.getSecretKey(keyID);
613
614        if (pgpSecKey == null) {
615            return null;
616        }
617        PBESecretKeyDecryptor decryptor = new BcPBESecretKeyDecryptorBuilder(
618          new BcPGPDigestCalculatorProvider()
619        ).build(pass);
620
621        return pgpSecKey.extractPrivateKey(decryptor);
622    }
623
624    private static byte[] hash (String s) throws NoSuchAlgorithmException {
625        MessageDigest md = MessageDigest.getInstance("SHA-1");
626        return md.digest(s.getBytes(StandardCharsets.UTF_8));
627    }
628}