001// Copyright (C) 2002 IAIK 002// https://sic.tech/ 003// 004// Copyright (C) 2003 - 2025 Stiftung Secure Information and 005// Communication Technologies SIC 006// https://sic.tech/ 007// 008// All rights reserved. 009// 010// This source is provided for inspection purposes and recompilation only, 011// unless specified differently in a contract with IAIK. This source has to 012// be kept in strict confidence and must not be disclosed to any third party 013// under any circumstances. Redistribution in source and binary forms, with 014// or without modification, are <not> permitted in any case! 015// 016// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 017// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 018// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 019// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 020// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 021// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 022// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 023// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 024// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 025// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 026// SUCH DAMAGE.// Copyright (C) 2002 IAIK 027// https://sic.tech 028// 029// Copyright (C) 2003 - 2025 Stiftung Secure Information and 030// Communication Technologies SIC 031// https://sic.tech 032// 033// All rights reserved. 034// 035// This source is provided for inspection purposes and recompilation only, 036// unless specified differently in a contract with IAIK. This source has to 037// be kept in strict confidence and must not be disclosed to any third party 038// under any circumstances. Redistribution in source and binary forms, with 039// or without modification, are <not> permitted in any case! 040// 041// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 042// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 043// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 044// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 045// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 046// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 047// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 048// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 049// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 050// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 051// SUCH DAMAGE. 052// 053// $Header: /IAIK-CMS/current/src/demo/smime/ess/SignedReceiptDemo.java 39 12.02.25 17:59 Dbratko $ 054// $Revision: 39 $ 055// 056 057package demo.smime.ess; 058 059import iaik.asn1.structures.AlgorithmID; 060import iaik.asn1.structures.Attribute; 061import iaik.cms.IssuerAndSerialNumber; 062import iaik.cms.SignerInfo; 063import iaik.smime.SMimeSignerInfo; 064import iaik.smime.SignedContent; 065import iaik.smime.ess.ContentIdentifier; 066import iaik.smime.ess.ESSException; 067import iaik.smime.ess.EntityIdentifier; 068import iaik.smime.ess.MLData; 069import iaik.smime.ess.MLExpansionHistory; 070import iaik.smime.ess.MLReceiptPolicy; 071import iaik.smime.ess.Receipt; 072import iaik.smime.ess.ReceiptContent; 073import iaik.smime.ess.ReceiptRequest; 074import iaik.smime.ess.ReceiptsFrom; 075import iaik.smime.ess.utils.SenderAndReceiptContentDigest; 076import iaik.smime.ess.utils.SignedReceipt; 077import iaik.x509.X509Certificate; 078 079import java.io.ByteArrayInputStream; 080import java.io.ByteArrayOutputStream; 081import java.io.IOException; 082import java.io.OutputStream; 083import java.security.NoSuchAlgorithmException; 084import java.security.PrivateKey; 085import java.security.PublicKey; 086import java.security.SignatureException; 087import java.util.Date; 088 089import jakarta.mail.Message; 090import jakarta.mail.MessagingException; 091import jakarta.mail.Session; 092import jakarta.mail.internet.InternetAddress; 093import jakarta.mail.internet.MimeMessage; 094 095import demo.DemoSMimeUtil; 096import demo.DemoUtil; 097import demo.keystore.CMSKeyStore; 098 099 100/** 101 * An <a href="https://www.rfc-editor.org/rfc/rfc2634.html" target = "_blank">RFC2634</a> ESS ReceiptRequest -- SignedReceipt demo. 102 * <p> 103 * This demo creates a message with a {@link iaik.smime.ess.ReceiptRequest 104 * ReceiptRequest} attribute and "sends" it to some intended recipient. 105 * The recipient then "sends" a signed receipt message back to the 106 * original sender who finally validates the signed receipt. 107 * A further test run adds a MLA layer with a {@link iaik.smime.ess.MLExpansionHistory 108 * MLExpansionHistory} attribute, 109 * that supersedes the original receipt request. 110 * <p> 111 * To run this demo the following packages are required: 112 * <ul> 113 * <li> 114 * <code>iaik_cms.jar</code> 115 * </li> 116 * <li> 117 * <code>iaik_jce(_full).jar</code> (<a href="https://sic.tech/products/core-crypto-toolkits/jca-jce/" target="_blank">IAIK-JCE Core Crypto Library</a>). 118 * </li> 119 * <li> 120 * <a href="https://jakarta.ee/specifications/mail/" target="_blank">Jakarta</a>/<a href="https://eclipse-ee4j.github.io/angus-mail/" target="_blank">Angus</a> Mail 121 * </li> 122 * <li> 123 * <a href="https://jakarta.ee/specifications/activation/" target="_blank">Jakarta Activation Framework</a> 124 * </li> 125 * </ul> 126 * 127 * @see iaik.smime.ess.ReceiptRequest 128 * @see iaik.smime.ess.Receipt 129 * @see iaik.smime.ess.MLExpansionHistory 130 * @see iaik.smime.ess.MLData 131 * @see iaik.smime.ess.MLReceiptPolicy 132 * @see iaik.smime.ess.utils.SignedReceipt 133 */ 134public class SignedReceiptDemo { 135 136 // whether to dump all generates test messages to System.out 137 final static boolean DUMP_MESSAGES = false; 138 139 // sender (sends a receipt request message) 140 String senderName_ = "IAIK Demo Sender"; 141 // recipient (receives a receipt request and sends a signed receipt) 142 String recipientName_ = "IAIK Demo Recipient"; 143 // an mail list agent 144 String mlaName_ = "IAIK Demo ML Agent"; 145 // we use the same email address for all parties 146 String senderAddress_ = "\""+senderName_+"\" <smimetest@iaik.at>"; 147 String recipientAddress_ = "\""+recipientName_+"\" <smimetest@iaik.at>"; 148 String mlaAddress_ = "\""+mlaName_+"\" <smimetest@iaik.tugraz.at>"; 149 150 String host_ = "mailhost"; // name of the mailhost 151 152 X509Certificate[] signerCertificates_; // signer certificate list 153 X509Certificate signerCertificate_; // certificate of the signer/sender 154 X509Certificate encryptionCertOfSigner_; // signer uses different certificate for encryption 155 PrivateKey signerPrivateKey_; // private key of the signer/sender 156 157 X509Certificate[] recipientCertificates_; // recipient certificate list 158 X509Certificate recipientCertificate_; // certificate of the recipient 159 X509Certificate encryptionCertOfRecipient_; // recipient uses different certificate for encryption 160 PrivateKey recipientPrivateKey_; // private key of the recipient 161 162 X509Certificate[] signerCertificatesOfMLA_; // signer certificates of MLA 163 PrivateKey signerPrivateKeyOfMLA_; // signer private key of MLA 164 165 166 /** 167 * Empty default constructor. Reads all required keys and certificates 168 * from the demo keystore (created by running @link demo.keystore.SetupCMSKeySrore) 169 * stored at "cms.keystore" in your current working directoy. 170 */ 171 public SignedReceiptDemo() { 172 173 System.out.println(); 174 System.out.println("******************************************************************************************"); 175 System.out.println("* SignedReceiptDemo *"); 176 System.out.println("* (shows the usage of the IAIK-CMS library for handling ESS signed receipts) *"); 177 System.out.println("******************************************************************************************"); 178 System.out.println(); 179 180 // get the certificates from the KeyStore 181 signerCertificates_ = CMSKeyStore.getCertificateChain(CMSKeyStore.RSA, CMSKeyStore.SZ_2048_SIGN_1); 182 signerPrivateKey_ = CMSKeyStore.getPrivateKey(CMSKeyStore.RSA, CMSKeyStore.SZ_2048_SIGN_1); 183 signerCertificate_ = signerCertificates_[0]; 184 encryptionCertOfSigner_ = CMSKeyStore.getCertificateChain(CMSKeyStore.RSA, CMSKeyStore.SZ_2048_CRYPT_1)[0]; 185 recipientCertificates_ = CMSKeyStore.getCertificateChain(CMSKeyStore.RSA, CMSKeyStore.SZ_2048_SIGN_2); 186 recipientPrivateKey_ = CMSKeyStore.getPrivateKey(CMSKeyStore.RSA, CMSKeyStore.SZ_2048_SIGN_2); 187 recipientCertificate_ = recipientCertificates_[0]; 188 encryptionCertOfRecipient_ = CMSKeyStore.getCertificateChain(CMSKeyStore.RSA, CMSKeyStore.SZ_2048_CRYPT_2)[0]; 189 signerCertificatesOfMLA_ = CMSKeyStore.getCertificateChain(CMSKeyStore.DSA, CMSKeyStore.SZ_2048_SIGN_1); 190 signerPrivateKeyOfMLA_ = CMSKeyStore.getPrivateKey(CMSKeyStore.DSA, CMSKeyStore.SZ_2048_SIGN_1); 191 X509Certificate[] tmpCerts = new X509Certificate[signerCertificates_.length + 1]; 192 System.arraycopy(signerCertificates_, 0, tmpCerts, 0, signerCertificates_.length); 193 tmpCerts[signerCertificates_.length] = encryptionCertOfSigner_; 194 signerCertificates_ = tmpCerts; 195 tmpCerts = new X509Certificate[recipientCertificates_.length + 1]; 196 System.arraycopy(recipientCertificates_, 0, tmpCerts, 0, recipientCertificates_.length); 197 tmpCerts[recipientCertificates_.length] = encryptionCertOfRecipient_; 198 recipientCertificates_ = tmpCerts; 199 } 200 201 /** 202 * Starts the SignedReceipt demo. 203 */ 204 public void start() { 205 boolean implicit = true; 206 boolean mla = false; 207 System.out.println("Testing receipt request - signed receipt (all implicit)"); 208 test(implicit, mla); 209 mla = true; 210 System.out.println("Testing receipt request - MLA - signed receipt (all implicit)"); 211 test(implicit, mla); 212 implicit = false; 213 mla = false; 214 System.out.println("Testing receipt request - signed receipt (all explicit)"); 215 test(implicit, mla); 216 mla = true; 217 System.out.println("Testing receipt request - MLA - signed receipt (all explicit)"); 218 test(implicit, mla); 219 System.out.println("Ready!"); 220 } 221 222 /** 223 * Runs the ReceiptRequest - SignedReceipt test. 224 * 225 * @param implicit whether to create implicit (application/pkcs7-mime) or 226 * explicit (multipart/signed) ReceiptRequest messages 227 * 228 * @param mla whether to add a MLA layer 229 */ 230 public void test(boolean implicit, boolean mla) { 231 232 try { 233 // get the default Session 234 Session session = DemoSMimeUtil.getSession(); 235 236 Message msg; // the message to send 237 ByteArrayOutputStream baos = new ByteArrayOutputStream(); // we write to a stream 238 ByteArrayInputStream bais; // we read from a stream 239 240 // we send a signed message with a receipt request 241 System.out.println("Creating implicit signed message with receipt request."); 242 // create message and "send" it (write it to baos) 243 msg = createSignedMessageWithReceiptRequest(session, implicit, baos); 244 245 // now parse the receipt request message 246 bais = new ByteArrayInputStream(baos.toByteArray()); 247 msg = new MimeMessage(session, bais); 248 if (DUMP_MESSAGES) { 249 dumpMessage(msg); 250 } 251 baos.reset(); 252 253 if (mla == true) { 254 // MLA receives the message and adds a MLExpansionHistory that supersedes the 255 // original receipt request 256 System.out.println("MLA: parsing received original message."); 257 SignedContent sc = (SignedContent)msg.getContent(); 258 System.out.println("MLA: Verifying signature."); 259 // verify the signature (we assume only one signer) 260 X509Certificate signer = sc.verify(); 261 System.out.println("MLA: This message is signed from: "+signer.getSubjectDN()); 262 // add MLExpansionHistory 263 System.out.println("MLA: Creating new signed message with MLExpansionHistory attribute."); 264 SignedContent mlaSc = new SignedContent(sc, implicit); 265 mlaSc.setCertificates(signerCertificatesOfMLA_); 266 try { 267 SMimeSignerInfo signerInfo = new SMimeSignerInfo(signerCertificatesOfMLA_[0], 268 (AlgorithmID)AlgorithmID.sha256.clone(), 269 (AlgorithmID)AlgorithmID.dsaWithSHA256.clone(), 270 signerPrivateKeyOfMLA_); 271 // add a MLExpansionHistory attribute superseding the original receipt request 272 MLExpansionHistory mlExpansionHistory = createMLExpansionHistory(signerCertificatesOfMLA_[0], 273 new Date(), 274 mlaAddress_); 275 signerInfo.addSignedAttribute(new Attribute(mlExpansionHistory)); 276 mlaSc.addSigner(signerInfo); 277 } catch (NoSuchAlgorithmException ex) { 278 throw new MessagingException("Algorithm not supported: " + ex.getMessage(), ex); 279 } 280 msg = createMessage(session, mlaAddress_, recipientAddress_, "IAIK-S/MIME: MLA with ReceiptRequest"); 281 msg.setContent(mlaSc, mlaSc.getContentType()); 282 // let the SignedContent update some message headers 283 mlaSc.setHeaders(msg); 284 msg.saveChanges(); 285 msg.writeTo(baos); 286 287 // now parse the MLA message 288 bais = new ByteArrayInputStream(baos.toByteArray()); 289 msg = new MimeMessage(session, bais); 290 291 if (DUMP_MESSAGES) { 292 dumpMessage(msg); 293 } 294 } 295 296 // signed receipt creation 297 baos.reset(); 298 Message msg1 = createMessageWithSignedReceipt(session, msg); 299 msg1.saveChanges(); 300 msg1.writeTo(baos); 301 bais = new ByteArrayInputStream(baos.toByteArray()); 302 msg = new MimeMessage(session, bais); 303 304 if (DUMP_MESSAGES) { 305 dumpMessage(msg); 306 } 307 // signed receipt validation 308 System.out.println("\nNow getting and verifying signed receipt:"); 309 verifyReceiptContent(msg); 310 311 } catch (Exception ex) { 312 ex.printStackTrace(); 313 throw new RuntimeException(ex.toString()); 314 } 315 316 317 } 318 319 /** 320 * Creates a MimeMessage. 321 * 322 * @param session the current mail session 323 * @param from the sender of the message 324 * @param to the recipient of the message 325 * @param subject the subject of the message 326 * 327 * @return the newly created MimeMessage 328 * 329 * @throws MessagingException if an error occurs when creating the message 330 */ 331 public Message createMessage(Session session, String from, String to, String subject) throws MessagingException { 332 MimeMessage msg = new MimeMessage(session); 333 msg.setFrom(new InternetAddress(from)); 334 msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to, false)); 335 msg.setSentDate(new Date()); 336 return msg; 337 } 338 339 /** 340 * Creates a signed message that contains a <code>ReceiptRequest</code> attribute. 341 * 342 * @param session the current mail session 343 * @param implicit whether to sign the content implicitly or explicitly 344 * @param os the output stream to which to write the message 345 * 346 * @return the message containing a ReceiptRequest attribute 347 */ 348 public Message createSignedMessageWithReceiptRequest(Session session, 349 boolean implicit, 350 OutputStream os) 351 throws Exception { 352 353 Message msg = createMessage(session, senderAddress_, recipientAddress_, "IAIK-S/MIME: ReceiptRequest"); 354 355 // create the inner signed content 356 SignedContent sc = new SignedContent(implicit, implicit ? SignedContent.SIGNED_DATA : null); 357 sc.setText("This is a signed message with a ReceiptRequest."); 358 359 sc.setCertificates(signerCertificates_); 360 SMimeSignerInfo signerInfo = new SMimeSignerInfo(signerCertificate_, 361 (AlgorithmID)AlgorithmID.sha256.clone(), 362 (AlgorithmID)AlgorithmID.rsaEncryption.clone(), 363 signerPrivateKey_, 364 encryptionCertOfSigner_, 365 true); 366 // add a ReceiptRequest attribute to request a receipt to be sent back to the sender 367 ReceiptRequest receiptRequest = createReceiptRequest(signerCertificate_.getPublicKey(), 368 msg.getSentDate(), 369 senderAddress_); 370 signerInfo.addSignedAttribute(new Attribute(receiptRequest)); 371 sc.addSigner(signerInfo); 372 msg.setContent(sc, sc.getContentType()); 373 // let the SignedContent update some message headers 374 sc.setHeaders(msg); 375 msg.saveChanges(); 376 msg.writeTo(os); 377 // now after sending (writing) the message we can access and keep the digest values for later SignedReceipt validation 378 storeDigestValues(sc); 379 return msg; 380 } 381 382 /** 383 * Creates a ReceiptRequest attribute to request all recipients to send 384 * a signed receipt to the entity to the given email address. 385 * 386 * @param publicKey the public key of the sender (used for ContentIdentifier calculation) 387 * @param sentDate the sent date of the message (used for ContentIdentifier calculation) 388 * @param email the email address of the sender (to whom to return a signed receipt) 389 * 390 * @return the ReceiptRequest 391 */ 392 public ReceiptRequest createReceiptRequest(PublicKey publicKey, Date sentDate, String email) { 393 // we request a receipt from all recipients 394 ReceiptsFrom receiptsFrom = new ReceiptsFrom(ReceiptsFrom.ALL_RECIPIENTS); 395 // the receipt should be send to the given email 396 String[] sendTo = { email }; 397 // create the signed content identifier 398 ContentIdentifier contentIdentifier = new ContentIdentifier(publicKey, sentDate, null); 399 // create the receipt request 400 ReceiptRequest receiptRequest = new ReceiptRequest(contentIdentifier, receiptsFrom, sendTo); 401 return receiptRequest; 402 } 403 404 /** 405 * Creates a MLExpansionHistory containing only one MLData for 406 * the given MLA with given expansion time and a MLReceiptPolicy 407 * of type IN_ADDITION_TO for the given mlaEmailAddress. 408 * 409 * @param mlaCertificate the certificate of the MLA from which to create the 410 * MLData EntityIdentiifier of type IssuerAndSerialNumber 411 * @param expansionTime the expansion time 412 * @param mlaEmailAddress to be set as IN_ADDITION_TO recipient list for 413 * the MLData MLRecipientPolicy 414 * 415 * @return the newly created MLExpansionHistory 416 */ 417 public static MLExpansionHistory createMLExpansionHistory(X509Certificate mlaCertificate, 418 Date expansionTime, 419 String mlaEmailAddress) { 420 421 IssuerAndSerialNumber ias = new IssuerAndSerialNumber(mlaCertificate); 422 MLData mlData = new MLData(new EntityIdentifier(ias), expansionTime); 423 MLReceiptPolicy mlReceiptPolicy = new MLReceiptPolicy(MLReceiptPolicy.IN_ADDITION_TO); 424 mlReceiptPolicy.setRecipientList(new String[] { mlaEmailAddress }); 425 mlData.setMLReceiptPolicy(mlReceiptPolicy); 426 return new MLExpansionHistory(mlData); 427 } 428 429 /** 430 * Keeps the signature message digest value of the sender and the receipt content digest 431 * values for later SignedReceipt validation. 432 * 433 * @param signedContent the signed message for which to keep the digest values 434 * 435 * @throws ESSException if an error occurs while gathering the required digest values 436 */ 437 public void storeDigestValues(SignedContent signedContent) throws ESSException { 438 SignerInfo originatorSignerInfo = signedContent.getSignerInfos()[0]; 439 SenderAndReceiptContentDigest.storeEntry(new SenderAndReceiptContentDigest(originatorSignerInfo)); 440 } 441 442 /** 443 * Creates a signed-receipt message from the received message. 444 * 445 * @param session the current mail session 446 * @param receivedMsg the message containing a ReceiptRequest attribute 447 * 448 * @return a message containing s signed receipt to be sent in return 449 * to the receipt request 450 * 451 * @throws Exception if some error occurs during receipt request processing 452 * or signed receipt creation 453 */ 454 public Message createMessageWithSignedReceipt(Session session, Message receivedMsg) 455 throws Exception { 456 457 SignedReceipt signedReceipt = new SignedReceipt(receivedMsg, recipientAddress_, System.out); 458 String subject = "IAIK-S/MIME: Signed Receipt"; 459 460 Message msg = signedReceipt.createReceiptMessage(recipientPrivateKey_, 461 recipientCertificates_, 462 recipientCertificates_[0], 463 (AlgorithmID)AlgorithmID.sha256.clone(), 464 (AlgorithmID)AlgorithmID.rsaEncryption.clone(), 465 encryptionCertOfRecipient_, 466 true, 467 session, 468 subject); 469 return msg; 470 } 471 472 473 /** 474 * Validates a signed receipt message received in return to a receipt request 475 * message. 476 * 477 * @param receiptMsg the message containing the signed receipt 478 * 479 * @throws Exception if the receipt validation fails for some reason 480 */ 481 public void verifyReceiptContent(Message receiptMsg) throws Exception { 482 // we assume to already know of the signed content 483 ReceiptContent receiptContent = (ReceiptContent)receiptMsg.getContent(); 484 485 Receipt receipt = (Receipt)receiptContent.getContent(); 486 System.out.println("\nReceipt received:"); 487 System.out.println(receipt); 488 489 490 // verify the signature (we assume only one signer) 491 X509Certificate receiptSigner = null; 492 try { 493 receiptSigner = receiptContent.verify(); 494 System.out.println("This receipt content is signed from: "+receiptSigner.getSubjectDN()); 495 } catch (SignatureException ex) { 496 System.err.println("Signature verification error!"); 497 throw ex; 498 } 499 500 501 try { 502 SenderAndReceiptContentDigest sarcd = SenderAndReceiptContentDigest.validateReceiptContent(receiptContent); 503 // now after validation we may remove the kept digest values from the repository 504 SenderAndReceiptContentDigest.removeEntry(sarcd); 505 } catch (ESSException ex) { 506 System.err.println("Signed Receipt validation error!"); 507 throw ex; 508 } 509 System.out.println("ReceiptContent successful validated!"); 510 } 511 512 /** 513 * Dumps the given message to System.out. 514 * 515 * @param msg the message to be dumped 516 * 517 * @throws Exception if some error occurs 518 */ 519 private static void dumpMessage(Message msg) throws Exception { 520 System.out.println("******************************************************************"); 521 System.out.println("Message dump: \n"); 522 msg.writeTo(System.out); 523 System.out.println("******************************************************************"); 524 } 525 526 /** 527 * Main method. 528 */ 529 public static void main(String[] argv) throws IOException { 530 531 try { 532 DemoSMimeUtil.initDemos(); 533 (new SignedReceiptDemo()).start(); 534 } catch (Exception ex) { 535 ex.printStackTrace(); 536 } 537 538 DemoUtil.waitKey(); 539 540 } 541}