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}