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.
027//
028// $Header: /IAIK-CMS/current/src/demo/cms/signedData/PssSignedDataDemo.java 21    12.02.25 17:58 Dbratko $
029// $Revision: 21 $
030//
031
032package demo.cms.signedData;
033
034import java.io.ByteArrayInputStream;
035import java.io.ByteArrayOutputStream;
036import java.io.IOException;
037import java.io.InputStream;
038import java.security.AlgorithmParameters;
039import java.security.InvalidAlgorithmParameterException;
040import java.security.MessageDigest;
041import java.security.NoSuchAlgorithmException;
042import java.security.PrivateKey;
043import java.security.SignatureException;
044import java.security.cert.Certificate;
045import java.security.spec.InvalidParameterSpecException;
046
047import demo.DemoUtil;
048import demo.keystore.CMSKeyStore;
049import iaik.asn1.ObjectID;
050import iaik.asn1.structures.AlgorithmID;
051import iaik.asn1.structures.Attribute;
052import iaik.cms.CMSException;
053import iaik.cms.IssuerAndSerialNumber;
054import iaik.cms.SecurityProvider;
055import iaik.cms.SignedData;
056import iaik.cms.SignedDataStream;
057import iaik.cms.SignerInfo;
058import iaik.cms.SubjectKeyID;
059import iaik.cms.attributes.CMSContentType;
060import iaik.cms.attributes.SigningTime;
061import iaik.pkcs.pkcs1.MGF1ParameterSpec;
062import iaik.pkcs.pkcs1.MaskGenerationAlgorithm;
063import iaik.pkcs.pkcs1.RSAPssParameterSpec;
064import iaik.utils.Util;
065import iaik.x509.X509Certificate;
066import iaik.x509.X509ExtensionException;
067
068/**
069 * This class demonstrates the CMS SignedData implementation for
070 * the RSA-PSS (PKCS#1v2.1) algorithm.
071 */
072public class PssSignedDataDemo {
073
074  // certificate of user 1
075  X509Certificate user1;
076  // private key of user 1
077  PrivateKey user1_pk;
078  // certificate of user 2
079  X509Certificate user2;
080  // private key of user 2
081  PrivateKey user2_pk;
082 
083  // a certificate chain containing the user certs + CA
084  Certificate[] certificates;
085  Certificate[] certs;
086  Certificate[] user1Certs;
087  
088  // just for attribute certificate testing
089  PrivateKey issuer1_pk;
090
091  /**
092   * Setups the demo certificate chains.
093   * 
094   * Keys and certificate are retrieved from the demo KeyStore.
095   * 
096   * @throws IOException if an file read error occurs
097   */
098  public PssSignedDataDemo() throws IOException {
099    
100    System.out.println();
101    System.out.println("**********************************************************************************");
102    System.out.println("*                             PssSignedDataDemo                                  *");
103    System.out.println("* (shows the usage of the CMS SignedData type with the RSA PSS signature scheme) *");
104    System.out.println("**********************************************************************************");
105    System.out.println();
106    
107    // add all certificates to the list
108    user1Certs = CMSKeyStore.getCertificateChain(CMSKeyStore.RSA, CMSKeyStore.SZ_2048_SIGN_1);
109    user1 = (X509Certificate)user1Certs[0];
110    user1_pk = CMSKeyStore.getPrivateKey(CMSKeyStore.RSA, CMSKeyStore.SZ_2048_SIGN_1);
111    user2 = CMSKeyStore.getCertificateChain(CMSKeyStore.RSA, CMSKeyStore.SZ_2048_SIGN_1)[0];
112    user2_pk = CMSKeyStore.getPrivateKey(CMSKeyStore.RSA, CMSKeyStore.SZ_2048_SIGN_1);
113      
114    certs = user1Certs;
115    certificates = new Certificate[certs.length+1];
116    System.arraycopy(certs, 0, certificates, 0, certs.length);
117    certificates[certs.length] = user2;
118  }
119  
120  /**
121   * Creates a CMS <code>SignedData</code> object.
122   * <p>
123   *
124   * @param message the message to be signed, as byte representation
125   * @param mode the transmission mode, either IMPLICIT or EXPLICIT
126   * @return the BER encoding of the <code>SignedData</code> object just created
127   * @throws CMSException if the <code>SignedData</code> object cannot
128   *                          be created
129   * @throws IOException if some stream I/O error occurs
130   */
131  public byte[] createSignedDataStream(byte[] message, int mode) throws CMSException, IOException  {
132    
133    System.out.print("Create a new message signed by user 1 :");
134   
135    // we are testing the stream interface
136    ByteArrayInputStream is = new ByteArrayInputStream(message);
137    // create a new SignedData object which includes the data
138    SignedDataStream signed_data = new SignedDataStream(is, mode);
139    
140    // SignedData shall include the certificate chain for verifying
141    signed_data.setCertificates(certificates);
142    
143    // cert at index 0 is the user certificate
144    IssuerAndSerialNumber issuer = new IssuerAndSerialNumber(user1);
145
146    // create a new SignerInfo for RSA-PSS with default parameters
147    SignerInfo signer_info = new SignerInfo(issuer, 
148                                            (AlgorithmID)AlgorithmID.sha256.clone(),
149                                            (AlgorithmID)AlgorithmID.rsassaPss.clone(),
150                                            user1_pk);
151    // create some signed attributes
152    // the message digest attribute is automatically added
153    Attribute[] attributes = new Attribute[2];
154    try {
155      // content type is data
156      CMSContentType contentType = new CMSContentType(ObjectID.cms_data);
157      attributes[0] = new Attribute(contentType);
158      // signing time is now
159      SigningTime signingTime = new SigningTime();
160      attributes[1] = new Attribute(signingTime);
161    } catch (Exception ex) {
162      throw new CMSException("Error creating attribute: " + ex.toString());   
163    }
164      
165    // set the attributes
166    signer_info.setSignedAttributes(attributes);
167    // finish the creation of SignerInfo by calling method addSigner
168    try {
169      signed_data.addSignerInfo(signer_info);
170      // another SignerInfo without signed attributes and RSA-PSS with user defined parameters
171      AlgorithmID hashID = (AlgorithmID)AlgorithmID.sha256.clone();
172      AlgorithmID mgfID = (AlgorithmID)AlgorithmID.mgf1.clone();
173      int saltLength = 32;
174      AlgorithmID rsaPssID = null;
175      try {
176        rsaPssID = createPssAlgorithmID(hashID, mgfID, saltLength);
177      } catch (Exception ex) {
178        throw new CMSException("Error creating PSS parameters: " + ex.toString());
179      }   
180      signer_info = new SignerInfo(new SubjectKeyID(user2), 
181                                   hashID, 
182                                   rsaPssID,
183                                   user2_pk);
184      
185      // the message digest itself is protected
186      signed_data.addSignerInfo(signer_info);
187
188    } catch (NoSuchAlgorithmException ex) {
189      throw new CMSException("No implementation for signature algorithm: "+ex.getMessage());
190    } catch (X509ExtensionException ex) {
191      throw new CMSException("Cannot create SubjectKeyID for user2 : " + ex.getMessage());
192    }
193
194    // write the data through SignedData to any out-of-band place
195    if (mode == SignedDataStream.EXPLICIT) {
196      InputStream data_is = signed_data.getInputStream();
197      byte[] buf = new byte[1024];
198      int r;
199      while ((r = data_is.read(buf)) > 0) {
200        ;   // skip data
201      }  
202    }
203
204    // return the SignedData as DER encoded byte array with block size 2048
205    ByteArrayOutputStream os = new ByteArrayOutputStream();
206    signed_data.writeTo(os, 2048);
207    return os.toByteArray();
208  }
209  
210
211  /**
212   * Parses a CMS <code>SignedData</code> object and verifies the signatures
213   * for all participated signers.
214   *
215   * @param signedData <code>SignedData</code> object as BER encoded byte array
216   * @param message the message which was transmitted out-of-band (if explicit signed),
217   *                otherwise <code>null</code> (implicit signed)
218   *
219   * @return the inherent message as byte array
220   * @throws CMSException if any signature does not verify
221   * @throws IOException if some stream I/O error occurs
222   */
223  public byte[] getSignedDataStream(byte[] signedData, byte[] message) throws CMSException, IOException {
224
225    // we are testing the stream interface
226    ByteArrayInputStream is = new ByteArrayInputStream(signedData);
227    // create the SignedData object
228    SignedDataStream signed_data = new SignedDataStream(is);
229    
230    if (signed_data.getMode() == SignedDataStream.EXPLICIT) {
231      // explicitly signed; set the data stream for digesting the message
232      signed_data.setInputStream(new ByteArrayInputStream(message));
233    }
234
235    // get an InputStream for reading the signed content
236    InputStream data = signed_data.getInputStream();
237    ByteArrayOutputStream os = new ByteArrayOutputStream();
238    Util.copyStream(data, os, null);
239
240    System.out.println("SignedData contains the following signer information:");
241    SignerInfo[] signer_infos = signed_data.getSignerInfos();
242    
243    int numberOfSignerInfos = signer_infos.length;
244    if (numberOfSignerInfos == 0) {
245      String warning = "Warning: Unsigned message (no SignerInfo included)!";  
246      System.err.println(warning);
247      throw new CMSException(warning);
248    } else {
249      for (int i = 0; i < numberOfSignerInfos; i++) {
250        
251        try {
252          // verify the signed data using the SignerInfo at index i
253          X509Certificate signer_cert = signed_data.verify(i);
254          // if the signature is OK the certificate of the signer is returned
255          System.out.println("Signature OK from signer: "+signer_cert.getSubjectDN());
256          // get signed attributes
257          SigningTime signingTime = (SigningTime)signer_infos[i].getSignedAttributeValue(ObjectID.signingTime);
258          if (signingTime != null) {
259            System.out.println("This message has been signed at " + signingTime.get());
260          } 
261          CMSContentType contentType = (CMSContentType)signer_infos[i].getSignedAttributeValue(ObjectID.contentType);
262          if (contentType != null) {
263            System.out.println("The content has CMS content type " + contentType.get().getName());
264          }
265        } catch (SignatureException ex) {
266          // if the signature is not OK a SignatureException is thrown
267          System.out.println("Signature ERROR from signer: "+signed_data.getCertificate(signer_infos[i].getSignerIdentifier()).getSubjectDN());
268          throw new CMSException(ex.toString());
269        } 
270      }  
271    
272      // now check alternative signature verification
273      System.out.println("Now check the signature assuming that no certs have been included:");
274      try {
275         SignerInfo signer_info = signed_data.verify(user1);
276          // if the signature is OK the certificate of the signer is returned
277          System.out.println("Signature OK from signer: "+user1.getSubjectDN());
278          
279      } catch (SignatureException ex) {
280          // if the signature is not OK a SignatureException is thrown
281          System.out.println("Signature ERROR from signer: "+user1.getSubjectDN());
282          throw new CMSException(ex.toString());
283      }
284    
285    
286      try {
287        SignerInfo signer_info = signed_data.verify(user2);
288        // if the signature is OK the certificate of the signer is returned
289        System.out.println("Signature OK from signer: "+signed_data.getCertificate(signer_info.getSignerIdentifier()).getSubjectDN());
290          
291      } catch (SignatureException ex) {
292        // if the signature is not OK a SignatureException is thrown
293        System.out.println("Signature ERROR from signer: "+user2.getSubjectDN());
294        throw new CMSException(ex.toString());
295      }
296      // in practice we also would validate the signer certificate(s)  
297    }
298        
299    return os.toByteArray();
300  }
301  
302  
303  
304  /**
305   * Creates a CMS <code>SignedData</code> object.
306   * <p>
307   *
308   * @param message the message to be signed, as byte representation
309   * @param mode the mode, either SignedData.IMPLICIT or SignedData.EXPLICIT
310   * @return the DER encoded <code>SignedData</code> object 
311   * @throws CMSException if the <code>SignedData</code> object cannot
312   *                          be created
313   */
314  public byte[] createSignedData(byte[] message, int mode) throws CMSException {
315    
316    System.out.println("Create a new message signed by user 1 :");
317  
318    // create a new SignedData object which includes the data
319    SignedData signed_data = new SignedData(message, mode);
320    
321    // SignedData shall include the certificate chain for verifying
322    signed_data.setCertificates(certificates);
323  
324    // cert at index 0 is the user certificate
325    IssuerAndSerialNumber issuer = new IssuerAndSerialNumber(user1);
326
327    // create a new SignerInfo for RSA-PSS with default parameters
328    SignerInfo signer_info = new SignerInfo(issuer, 
329                                            (AlgorithmID)AlgorithmID.sha256.clone(),
330                                            (AlgorithmID)AlgorithmID.rsassaPss.clone(),
331                                            user1_pk);
332    
333    // create some signed attributes
334    // the message digest attribute is automatically added
335    Attribute[] attributes = new Attribute[2];
336    try {
337      // content type is data
338      CMSContentType contentType = new CMSContentType(ObjectID.cms_data);
339      attributes[0] = new Attribute(contentType);
340      // signing time is now
341      SigningTime signingTime = new SigningTime();
342      attributes[1] = new Attribute(signingTime);
343    } catch (Exception ex) {
344      throw new CMSException("Error creating attribute: " + ex.toString());   
345    }
346    
347    // set the attributes
348    signer_info.setSignedAttributes(attributes);
349    // finish the creation of SignerInfo by calling method addSigner
350    try {
351      signed_data.addSignerInfo(signer_info);
352
353      // another SignerInfo without signed attributes and RSA-PSS with user defined parameters
354      AlgorithmID hashID = (AlgorithmID)AlgorithmID.sha256.clone();
355      AlgorithmID mgfID = (AlgorithmID)AlgorithmID.mgf1.clone();
356      int saltLength = 32;
357      AlgorithmID rsaPssID = null;
358      try {
359        rsaPssID = createPssAlgorithmID(hashID, mgfID, saltLength);
360      } catch (Exception ex) {
361        throw new CMSException("Error creating PSS parameters: " + ex.toString());
362      }   
363      signer_info = new SignerInfo(new SubjectKeyID(user2), 
364                                   hashID, 
365                                   rsaPssID,
366                                   user2_pk);
367  
368      signed_data.addSignerInfo(signer_info);
369
370    } catch (NoSuchAlgorithmException ex) {
371      throw new CMSException("No implementation for signature algorithm: "+ex.getMessage());
372    } catch (X509ExtensionException ex) {
373      throw new CMSException("Cannot create SubjectKeyID for user2 : " + ex.getMessage());
374    }     
375    return signed_data.getEncoded();
376  }
377  
378  
379  /**
380   * Parses a CMS <code>SignedData</code> object and verifies the signatures
381   * for all participated signers.
382   *
383   * @param encoding the DER encoded <code>SignedData</code> object
384   * @param message the message which was transmitted out-of-band (if explicit signed),
385   *                otherwise <code>null</code> (implicit signed)
386   *
387   * @return the inherent message as byte array
388   * @throws CMSException if any signature does not verify
389   * @throws IOException if some stream I/O error occurs
390   */
391  public byte[] getSignedData(byte[] encoding, byte[] message) throws CMSException, IOException {
392    
393    // create the SignedData object
394    SignedData signed_data = new SignedData(new ByteArrayInputStream(encoding));
395    if (signed_data.getMode() == SignedData.EXPLICIT) {
396      // explicitly signed; set the data stream for digesting the message
397      signed_data.setContent(message);
398    }
399   
400    System.out.println("SignedData contains the following signer information:");
401    SignerInfo[] signer_infos = signed_data.getSignerInfos();
402    
403    int numberOfSignerInfos = signer_infos.length;
404    if (numberOfSignerInfos == 0) {
405      String warning = "Warning: Unsigned message (no SignerInfo included)!";  
406      System.err.println(warning);
407      throw new CMSException(warning);
408    } else {
409      for (int i = 0; i < numberOfSignerInfos; i++) {
410        try {
411          // verify the signed data using the SignerInfo at index i
412          X509Certificate signer_cert = signed_data.verify(i);
413          // if the signature is OK the certificate of the signer is returned
414          System.out.println("Signature OK from signer: "+signer_cert.getSubjectDN());
415          // get signed attributes
416          SigningTime signingTime = (SigningTime)signer_infos[i].getSignedAttributeValue(ObjectID.signingTime);
417          if (signingTime != null) {
418            System.out.println("This message has been signed at " + signingTime.get());
419          } 
420          CMSContentType contentType = (CMSContentType)signer_infos[i].getSignedAttributeValue(ObjectID.contentType);
421          if (contentType != null) {
422            System.out.println("The content has CMS content type " + contentType.get().getName());
423          }
424        } catch (SignatureException ex) {
425           // if the signature is not OK a SignatureException is thrown
426           System.out.println("Signature ERROR from signer: "+signed_data.getCertificate(signer_infos[i].getSignerIdentifier()).getSubjectDN());
427           throw new CMSException(ex.toString());
428        } 
429      }      
430    
431      // now check alternative signature verification
432      System.out.println("Now check the signature assuming that no certs have been included:");
433      try {
434        SignerInfo signer_info = signed_data.verify(user1);
435        // if the signature is OK the certificate of the signer is returned
436        System.out.println("Signature OK from signer: "+signed_data.getCertificate(signer_info.getSignerIdentifier()).getSubjectDN());
437          
438      } catch (SignatureException ex) {
439        // if the signature is not OK a SignatureException is thrown
440        System.out.println("Signature ERROR from signer: "+user1.getSubjectDN());
441        throw new CMSException(ex.toString());
442      }
443      try {
444        SignerInfo signer_info = signed_data.verify(user2);
445        // if the signature is OK the certificate of the signer is returned
446        System.out.println("Signature OK from signer: "+signed_data.getCertificate(signer_info.getSignerIdentifier()).getSubjectDN());
447          
448      } catch (SignatureException ex) {
449        // if the signature is not OK a SignatureException is thrown
450        System.out.println("Signature ERROR from signer: "+user2.getSubjectDN());
451        throw new CMSException(ex.toString());
452      }
453      // in practice we also would validate the signer certificate(s)  
454    }
455    return signed_data.getContent();
456  }
457  
458  /**
459   * Creates an RSA-PSS AlgorithmID with the supplied parameters (hash algorithm id,
460   * mask generation function, salt length).
461   *
462   * @param hashID the hash algorithm to be used
463   * @param mgfID the mask generation function to be used
464   * @param saltLength the salt length to be used
465   *
466   * @return the RSA-PSS algorithm id with the given parameters 
467   *
468   * @throws InvalidAlgorithmParameterException if the parameters cannot be created/set
469   * @throws NoSuchAlgorithmException if there is no AlgorithmParameters implementation
470   *                                     for RSA-PSS
471   */
472  public static AlgorithmID createPssAlgorithmID(AlgorithmID hashID, AlgorithmID mgfID, int saltLength)
473    throws InvalidAlgorithmParameterException, NoSuchAlgorithmException {
474    
475    SecurityProvider provider = SecurityProvider.getSecurityProvider();
476    AlgorithmID rsaPssID = (AlgorithmID)AlgorithmID.rsassaPss.clone();
477    mgfID.setParameter(hashID.toASN1Object());
478    // create a RSAPssParameterSpec
479    RSAPssParameterSpec pssParamSpec = new RSAPssParameterSpec(hashID, mgfID, saltLength);
480    // optionally set hash and mgf engines
481    MessageDigest hashEngine = provider.getMessageDigest(hashID);
482    pssParamSpec.setHashEngine(hashEngine);
483    MaskGenerationAlgorithm mgfEngine = provider.getMaskGenerationAlgorithm(mgfID);
484    MGF1ParameterSpec mgf1Spec = new MGF1ParameterSpec(hashID);
485    mgf1Spec.setHashEngine(hashEngine);
486    mgfEngine.setParameters(mgf1Spec);
487    pssParamSpec.setMGFEngine(mgfEngine);
488
489    AlgorithmParameters pssParams = null;
490    try {
491      pssParams = provider.getAlgorithmParameters(SecurityProvider.IMPLEMENTATION_NAME_RSA_PSS);
492      pssParams.init(pssParamSpec);
493    } catch (InvalidParameterSpecException ex) {
494      throw new InvalidAlgorithmParameterException("Cannot init PSS params: " + ex.getMessage());  
495    }    
496   
497    rsaPssID.setAlgorithmParameters(pssParams);
498    return rsaPssID;
499  }  
500
501  /**
502   * Tests the CMS SignedData implementation for
503   * the RSA-PSS (PKCS#1v2.1) algorithm.
504   */
505  public void start() {
506     // the test message
507    String m = "This is the test message.";
508    System.out.println("Test message: \""+m+"\"");
509    System.out.println();
510    byte[] message = m.getBytes();
511   
512    try {
513      byte[] encoding;
514      byte[] received_message = null;
515      System.out.println("Stream implementation demos (RSA-PPS signing)");
516      System.out.println("=============================================");
517      //
518      // test CMS Implicit SignedDataStream
519      //
520      System.out.println("\nImplicit SignedDataStream demo [create]:\n");
521      encoding = createSignedDataStream(message, SignedDataStream.IMPLICIT);
522      // transmit data
523      System.out.println("\nImplicit SignedDataStream demo [parse]:\n");
524      received_message = getSignedDataStream(encoding, null);
525      System.out.print("\nSigned content: ");
526      System.out.println(new String(received_message));
527      
528      //
529      // test CMS Explicit SignedDataStream
530      //
531      System.out.println("\nExplicit SignedDataStream demo [create]:\n");
532      encoding = createSignedDataStream(message, SignedDataStream.EXPLICIT);
533      // transmit data
534      System.out.println("\nExplicit SignedDataStream demo [parse]:\n");
535      received_message = getSignedDataStream(encoding, message);
536      System.out.print("\nSigned content: ");
537      System.out.println(new String(received_message));
538            
539      // the non-stream implementation
540      System.out.println("\nNon-stream implementation demos (RSA-PPS signing)");
541      System.out.println("===================================================");
542   
543      //
544      // test CMS Implicit SignedData
545      //
546      System.out.println("\nImplicit CMS SignedData demo [create]:\n");
547      encoding = createSignedData(message, SignedData.IMPLICIT);
548      // transmit data
549      System.out.println("\nImplicit CMS SignedData demo [parse]:\n");
550      received_message = getSignedData(encoding, null);
551      System.out.print("\nSigned content: ");
552      System.out.println(new String(received_message));
553
554      //
555      // test CMS Explicit SignedData
556      //
557      System.out.println("\nExplicit CMS SignedData demo [create]:\n");
558      encoding = createSignedData(message, SignedData.EXPLICIT);
559      // transmit data
560      System.out.println("\nExplicit CMS SignedData demo [parse]:\n");
561      received_message = getSignedData(encoding, message);
562      System.out.print("\nSigned content: ");
563      System.out.println(new String(received_message));
564      
565        } catch (Exception ex) {
566          ex.printStackTrace();
567          throw new RuntimeException(ex.toString());
568        }
569  }
570  
571  /**
572   * The main method.
573   */
574  public static void main(String argv[]) throws Exception {
575
576        DemoUtil.initDemos();
577    (new PssSignedDataDemo()).start();
578    System.out.println("\nReady!");
579    DemoUtil.waitKey();
580  }
581}