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}