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}