Home | History | Annotate | Download | only in provider
      1 /*
      2  * Copyright (c) 1998, 2011, Oracle and/or its affiliates. All rights reserved.
      3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
      4  *
      5  * This code is free software; you can redistribute it and/or modify it
      6  * under the terms of the GNU General Public License version 2 only, as
      7  * published by the Free Software Foundation.  Oracle designates this
      8  * particular file as subject to the "Classpath" exception as provided
      9  * by Oracle in the LICENSE file that accompanied this code.
     10  *
     11  * This code is distributed in the hope that it will be useful, but WITHOUT
     12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
     13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
     14  * version 2 for more details (a copy is included in the LICENSE file that
     15  * accompanied this code).
     16  *
     17  * You should have received a copy of the GNU General Public License version
     18  * 2 along with this work; if not, write to the Free Software Foundation,
     19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
     20  *
     21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
     22  * or visit www.oracle.com if you need additional information or have any
     23  * questions.
     24  */
     25 
     26 package sun.security.provider;
     27 
     28 import java.io.*;
     29 import java.util.*;
     30 import java.security.cert.*;
     31 import sun.security.x509.X509CertImpl;
     32 import sun.security.x509.X509CRLImpl;
     33 import sun.security.pkcs.PKCS7;
     34 import sun.security.provider.certpath.X509CertPath;
     35 import sun.security.provider.certpath.X509CertificatePair;
     36 import sun.security.util.DerValue;
     37 import sun.security.util.Cache;
     38 import sun.misc.BASE64Decoder;
     39 import sun.security.pkcs.ParsingException;
     40 
     41 /**
     42  * This class defines a certificate factory for X.509 v3 certificates &
     43  * certification paths, and X.509 v2 certificate revocation lists (CRLs).
     44  *
     45  * @author Jan Luehe
     46  * @author Hemma Prafullchandra
     47  * @author Sean Mullan
     48  *
     49  *
     50  * @see java.security.cert.CertificateFactorySpi
     51  * @see java.security.cert.Certificate
     52  * @see java.security.cert.CertPath
     53  * @see java.security.cert.CRL
     54  * @see java.security.cert.X509Certificate
     55  * @see java.security.cert.X509CRL
     56  * @see sun.security.x509.X509CertImpl
     57  * @see sun.security.x509.X509CRLImpl
     58  */
     59 
     60 public class X509Factory extends CertificateFactorySpi {
     61 
     62     public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
     63     public static final String END_CERT = "-----END CERTIFICATE-----";
     64 
     65     private static final int ENC_MAX_LENGTH = 4096 * 1024; // 4 MB MAX
     66 
     67     private static final Cache certCache = Cache.newSoftMemoryCache(750);
     68     private static final Cache crlCache = Cache.newSoftMemoryCache(750);
     69 
     70     /**
     71      * Generates an X.509 certificate object and initializes it with
     72      * the data read from the input stream <code>is</code>.
     73      *
     74      * @param is an input stream with the certificate data.
     75      *
     76      * @return an X.509 certificate object initialized with the data
     77      * from the input stream.
     78      *
     79      * @exception CertificateException on parsing errors.
     80      */
     81     public Certificate engineGenerateCertificate(InputStream is)
     82         throws CertificateException
     83     {
     84         if (is == null) {
     85             // clear the caches (for debugging)
     86             certCache.clear();
     87             X509CertificatePair.clearCache();
     88             throw new CertificateException("Missing input stream");
     89         }
     90         try {
     91             byte[] encoding = readOneBlock(is);
     92             if (encoding != null) {
     93                 X509CertImpl cert = (X509CertImpl)getFromCache(certCache, encoding);
     94                 if (cert != null) {
     95                     return cert;
     96                 }
     97                 cert = new X509CertImpl(encoding);
     98                 addToCache(certCache, cert.getEncodedInternal(), cert);
     99                 return cert;
    100             } else {
    101                 throw new IOException("Empty input");
    102             }
    103         } catch (IOException ioe) {
    104             throw (CertificateException)new CertificateException
    105             ("Could not parse certificate: " + ioe.toString()).initCause(ioe);
    106         }
    107     }
    108 
    109     /**
    110      * Read from the stream until length bytes have been read or EOF has
    111      * been reached. Return the number of bytes actually read.
    112      */
    113     private static int readFully(InputStream in, ByteArrayOutputStream bout,
    114             int length) throws IOException {
    115         int read = 0;
    116         byte[] buffer = new byte[2048];
    117         while (length > 0) {
    118             int n = in.read(buffer, 0, length<2048?length:2048);
    119             if (n <= 0) {
    120                 break;
    121             }
    122             bout.write(buffer, 0, n);
    123             read += n;
    124             length -= n;
    125         }
    126         return read;
    127     }
    128 
    129     /**
    130      * Return an interned X509CertImpl for the given certificate.
    131      * If the given X509Certificate or X509CertImpl is already present
    132      * in the cert cache, the cached object is returned. Otherwise,
    133      * if it is a X509Certificate, it is first converted to a X509CertImpl.
    134      * Then the X509CertImpl is added to the cache and returned.
    135      *
    136      * Note that all certificates created via generateCertificate(InputStream)
    137      * are already interned and this method does not need to be called.
    138      * It is useful for certificates that cannot be created via
    139      * generateCertificate() and for converting other X509Certificate
    140      * implementations to an X509CertImpl.
    141      */
    142     public static synchronized X509CertImpl intern(X509Certificate c)
    143             throws CertificateException {
    144         if (c == null) {
    145             return null;
    146         }
    147         boolean isImpl = c instanceof X509CertImpl;
    148         byte[] encoding;
    149         if (isImpl) {
    150             encoding = ((X509CertImpl)c).getEncodedInternal();
    151         } else {
    152             encoding = c.getEncoded();
    153         }
    154         X509CertImpl newC = (X509CertImpl)getFromCache(certCache, encoding);
    155         if (newC != null) {
    156             return newC;
    157         }
    158         if (isImpl) {
    159             newC = (X509CertImpl)c;
    160         } else {
    161             newC = new X509CertImpl(encoding);
    162             encoding = newC.getEncodedInternal();
    163         }
    164         addToCache(certCache, encoding, newC);
    165         return newC;
    166     }
    167 
    168     /**
    169      * Return an interned X509CRLImpl for the given certificate.
    170      * For more information, see intern(X509Certificate).
    171      */
    172     public static synchronized X509CRLImpl intern(X509CRL c)
    173             throws CRLException {
    174         if (c == null) {
    175             return null;
    176         }
    177         boolean isImpl = c instanceof X509CRLImpl;
    178         byte[] encoding;
    179         if (isImpl) {
    180             encoding = ((X509CRLImpl)c).getEncodedInternal();
    181         } else {
    182             encoding = c.getEncoded();
    183         }
    184         X509CRLImpl newC = (X509CRLImpl)getFromCache(crlCache, encoding);
    185         if (newC != null) {
    186             return newC;
    187         }
    188         if (isImpl) {
    189             newC = (X509CRLImpl)c;
    190         } else {
    191             newC = new X509CRLImpl(encoding);
    192             encoding = newC.getEncodedInternal();
    193         }
    194         addToCache(crlCache, encoding, newC);
    195         return newC;
    196     }
    197 
    198     /**
    199      * Get the X509CertImpl or X509CRLImpl from the cache.
    200      */
    201     private static synchronized Object getFromCache(Cache cache,
    202             byte[] encoding) {
    203         Object key = new Cache.EqualByteArray(encoding);
    204         Object value = cache.get(key);
    205         return value;
    206     }
    207 
    208     /**
    209      * Add the X509CertImpl or X509CRLImpl to the cache.
    210      */
    211     private static synchronized void addToCache(Cache cache, byte[] encoding,
    212             Object value) {
    213         if (encoding.length > ENC_MAX_LENGTH) {
    214             return;
    215         }
    216         Object key = new Cache.EqualByteArray(encoding);
    217         cache.put(key, value);
    218     }
    219 
    220     /**
    221      * Generates a <code>CertPath</code> object and initializes it with
    222      * the data read from the <code>InputStream</code> inStream. The data
    223      * is assumed to be in the default encoding.
    224      *
    225      * @param inStream an <code>InputStream</code> containing the data
    226      * @return a <code>CertPath</code> initialized with the data from the
    227      *   <code>InputStream</code>
    228      * @exception CertificateException if an exception occurs while decoding
    229      * @since 1.4
    230      */
    231     public CertPath engineGenerateCertPath(InputStream inStream)
    232         throws CertificateException
    233     {
    234         if (inStream == null) {
    235             throw new CertificateException("Missing input stream");
    236         }
    237         try {
    238             byte[] encoding = readOneBlock(inStream);
    239             if (encoding != null) {
    240                 return new X509CertPath(new ByteArrayInputStream(encoding));
    241             } else {
    242                 throw new IOException("Empty input");
    243             }
    244         } catch (IOException ioe) {
    245             throw new CertificateException(ioe.getMessage());
    246         }
    247     }
    248 
    249     /**
    250      * Generates a <code>CertPath</code> object and initializes it with
    251      * the data read from the <code>InputStream</code> inStream. The data
    252      * is assumed to be in the specified encoding.
    253      *
    254      * @param inStream an <code>InputStream</code> containing the data
    255      * @param encoding the encoding used for the data
    256      * @return a <code>CertPath</code> initialized with the data from the
    257      *   <code>InputStream</code>
    258      * @exception CertificateException if an exception occurs while decoding or
    259      *   the encoding requested is not supported
    260      * @since 1.4
    261      */
    262     public CertPath engineGenerateCertPath(InputStream inStream,
    263         String encoding) throws CertificateException
    264     {
    265         if (inStream == null) {
    266             throw new CertificateException("Missing input stream");
    267         }
    268         try {
    269             byte[] data = readOneBlock(inStream);
    270             if (data != null) {
    271                 return new X509CertPath(new ByteArrayInputStream(data), encoding);
    272             } else {
    273                 throw new IOException("Empty input");
    274             }
    275         } catch (IOException ioe) {
    276             throw new CertificateException(ioe.getMessage());
    277         }
    278     }
    279 
    280     /**
    281      * Generates a <code>CertPath</code> object and initializes it with
    282      * a <code>List</code> of <code>Certificate</code>s.
    283      * <p>
    284      * The certificates supplied must be of a type supported by the
    285      * <code>CertificateFactory</code>. They will be copied out of the supplied
    286      * <code>List</code> object.
    287      *
    288      * @param certificates a <code>List</code> of <code>Certificate</code>s
    289      * @return a <code>CertPath</code> initialized with the supplied list of
    290      *   certificates
    291      * @exception CertificateException if an exception occurs
    292      * @since 1.4
    293      */
    294     public CertPath
    295         engineGenerateCertPath(List<? extends Certificate> certificates)
    296         throws CertificateException
    297     {
    298         return(new X509CertPath(certificates));
    299     }
    300 
    301     /**
    302      * Returns an iteration of the <code>CertPath</code> encodings supported
    303      * by this certificate factory, with the default encoding first.
    304      * <p>
    305      * Attempts to modify the returned <code>Iterator</code> via its
    306      * <code>remove</code> method result in an
    307      * <code>UnsupportedOperationException</code>.
    308      *
    309      * @return an <code>Iterator</code> over the names of the supported
    310      *         <code>CertPath</code> encodings (as <code>String</code>s)
    311      * @since 1.4
    312      */
    313     public Iterator<String> engineGetCertPathEncodings() {
    314         return(X509CertPath.getEncodingsStatic());
    315     }
    316 
    317     /**
    318      * Returns a (possibly empty) collection view of X.509 certificates read
    319      * from the given input stream <code>is</code>.
    320      *
    321      * @param is the input stream with the certificates.
    322      *
    323      * @return a (possibly empty) collection view of X.509 certificate objects
    324      * initialized with the data from the input stream.
    325      *
    326      * @exception CertificateException on parsing errors.
    327      */
    328     public Collection<? extends java.security.cert.Certificate>
    329             engineGenerateCertificates(InputStream is)
    330             throws CertificateException {
    331         if (is == null) {
    332             throw new CertificateException("Missing input stream");
    333         }
    334         try {
    335             return parseX509orPKCS7Cert(is);
    336         } catch (IOException ioe) {
    337             throw new CertificateException(ioe);
    338         }
    339     }
    340 
    341     /**
    342      * Generates an X.509 certificate revocation list (CRL) object and
    343      * initializes it with the data read from the given input stream
    344      * <code>is</code>.
    345      *
    346      * @param is an input stream with the CRL data.
    347      *
    348      * @return an X.509 CRL object initialized with the data
    349      * from the input stream.
    350      *
    351      * @exception CRLException on parsing errors.
    352      */
    353     public CRL engineGenerateCRL(InputStream is)
    354         throws CRLException
    355     {
    356         if (is == null) {
    357             // clear the cache (for debugging)
    358             crlCache.clear();
    359             throw new CRLException("Missing input stream");
    360         }
    361         try {
    362             byte[] encoding = readOneBlock(is);
    363             if (encoding != null) {
    364                 X509CRLImpl crl = (X509CRLImpl)getFromCache(crlCache, encoding);
    365                 if (crl != null) {
    366                     return crl;
    367                 }
    368                 crl = new X509CRLImpl(encoding);
    369                 addToCache(crlCache, crl.getEncodedInternal(), crl);
    370                 return crl;
    371             } else {
    372                 throw new IOException("Empty input");
    373             }
    374         } catch (IOException ioe) {
    375             throw new CRLException(ioe.getMessage());
    376         }
    377     }
    378 
    379     /**
    380      * Returns a (possibly empty) collection view of X.509 CRLs read
    381      * from the given input stream <code>is</code>.
    382      *
    383      * @param is the input stream with the CRLs.
    384      *
    385      * @return a (possibly empty) collection view of X.509 CRL objects
    386      * initialized with the data from the input stream.
    387      *
    388      * @exception CRLException on parsing errors.
    389      */
    390     public Collection<? extends java.security.cert.CRL> engineGenerateCRLs(
    391             InputStream is) throws CRLException
    392     {
    393         if (is == null) {
    394             throw new CRLException("Missing input stream");
    395         }
    396         try {
    397             return parseX509orPKCS7CRL(is);
    398         } catch (IOException ioe) {
    399             throw new CRLException(ioe.getMessage());
    400         }
    401     }
    402 
    403     /*
    404      * Parses the data in the given input stream as a sequence of DER
    405      * encoded X.509 certificates (in binary or base 64 encoded format) OR
    406      * as a single PKCS#7 encoded blob (in binary or base64 encoded format).
    407      */
    408     private Collection<? extends java.security.cert.Certificate>
    409         parseX509orPKCS7Cert(InputStream is)
    410         throws CertificateException, IOException
    411     {
    412         Collection<X509CertImpl> coll = new ArrayList<>();
    413         byte[] data = readOneBlock(is);
    414         if (data == null) {
    415             return new ArrayList<>(0);
    416         }
    417         try {
    418             PKCS7 pkcs7 = new PKCS7(data);
    419             X509Certificate[] certs = pkcs7.getCertificates();
    420             // certs are optional in PKCS #7
    421             if (certs != null) {
    422                 return Arrays.asList(certs);
    423             } else {
    424                 // no crls provided
    425                 return new ArrayList<>(0);
    426             }
    427         } catch (ParsingException e) {
    428             while (data != null) {
    429                 coll.add(new X509CertImpl(data));
    430                 data = readOneBlock(is);
    431             }
    432         }
    433         return coll;
    434     }
    435 
    436     /*
    437      * Parses the data in the given input stream as a sequence of DER encoded
    438      * X.509 CRLs (in binary or base 64 encoded format) OR as a single PKCS#7
    439      * encoded blob (in binary or base 64 encoded format).
    440      */
    441     private Collection<? extends java.security.cert.CRL>
    442         parseX509orPKCS7CRL(InputStream is)
    443         throws CRLException, IOException
    444     {
    445         Collection<X509CRLImpl> coll = new ArrayList<>();
    446         byte[] data = readOneBlock(is);
    447         if (data == null) {
    448             return new ArrayList<>(0);
    449         }
    450         try {
    451             PKCS7 pkcs7 = new PKCS7(data);
    452             X509CRL[] crls = pkcs7.getCRLs();
    453             // CRLs are optional in PKCS #7
    454             if (crls != null) {
    455                 return Arrays.asList(crls);
    456             } else {
    457                 // no crls provided
    458                 return new ArrayList<>(0);
    459             }
    460         } catch (ParsingException e) {
    461             while (data != null) {
    462                 coll.add(new X509CRLImpl(data));
    463                 data = readOneBlock(is);
    464             }
    465         }
    466         return coll;
    467     }
    468 
    469     /**
    470      * Returns an ASN.1 SEQUENCE from a stream, which might be a BER-encoded
    471      * binary block or a PEM-style BASE64-encoded ASCII data. In the latter
    472      * case, it's de-BASE64'ed before return.
    473      *
    474      * After the reading, the input stream pointer is after the BER block, or
    475      * after the newline character after the -----END SOMETHING----- line.
    476      *
    477      * @param is the InputStream
    478      * @returns byte block or null if end of stream
    479      * @throws IOException If any parsing error
    480      */
    481     private static byte[] readOneBlock(InputStream is) throws IOException {
    482 
    483         // The first character of a BLOCK.
    484         int c = is.read();
    485         if (c == -1) {
    486             return null;
    487         }
    488         if (c == DerValue.tag_Sequence) {
    489             ByteArrayOutputStream bout = new ByteArrayOutputStream(2048);
    490             bout.write(c);
    491             readBERInternal(is, bout, c);
    492             return bout.toByteArray();
    493         } else {
    494             // Read BASE64 encoded data, might skip info at the beginning
    495             char[] data = new char[2048];
    496             int pos = 0;
    497 
    498             // Step 1: Read until header is found
    499             int hyphen = (c=='-') ? 1: 0;   // count of consequent hyphens
    500             int last = (c=='-') ? -1: c;    // the char before hyphen
    501             while (true) {
    502                 int next = is.read();
    503                 if (next == -1) {
    504                     // We accept useless data after the last block,
    505                     // say, empty lines.
    506                     return null;
    507                 }
    508                 if (next == '-') {
    509                     hyphen++;
    510                 } else {
    511                     hyphen = 0;
    512                     last = next;
    513                 }
    514                 if (hyphen == 5 && (last==-1 || last=='\r' || last=='\n')) {
    515                     break;
    516                 }
    517             }
    518 
    519             // Step 2: Read the rest of header, determine the line end
    520             int end;
    521             StringBuffer header = new StringBuffer("-----");
    522             while (true) {
    523                 int next = is.read();
    524                 if (next == -1) {
    525                     throw new IOException("Incomplete data");
    526                 }
    527                 if (next == '\n') {
    528                     end = '\n';
    529                     break;
    530                 }
    531                 if (next == '\r') {
    532                     next = is.read();
    533                     if (next == -1) {
    534                         throw new IOException("Incomplete data");
    535                     }
    536                     if (next == '\n') {
    537                         end = '\n';
    538                     } else {
    539                         end = '\r';
    540                         data[pos++] = (char)next;
    541                     }
    542                     break;
    543                 }
    544                 header.append((char)next);
    545             }
    546 
    547             // Step 3: Read the data
    548             while (true) {
    549                 int next = is.read();
    550                 if (next == -1) {
    551                     throw new IOException("Incomplete data");
    552                 }
    553                 if (next != '-') {
    554                     data[pos++] = (char)next;
    555                     if (pos >= data.length) {
    556                         data = Arrays.copyOf(data, data.length+1024);
    557                     }
    558                 } else {
    559                     break;
    560                 }
    561             }
    562 
    563             // Step 4: Consume the footer
    564             StringBuffer footer = new StringBuffer("-");
    565             while (true) {
    566                 int next = is.read();
    567                 // Add next == '\n' for maximum safety, in case endline
    568                 // is not consistent.
    569                 if (next == -1 || next == end || next == '\n') {
    570                     break;
    571                 }
    572                 if (next != '\r') footer.append((char)next);
    573             }
    574 
    575             checkHeaderFooter(header.toString(), footer.toString());
    576 
    577             BASE64Decoder decoder = new BASE64Decoder();
    578             return decoder.decodeBuffer(new String(data, 0, pos));
    579         }
    580     }
    581 
    582     private static void checkHeaderFooter(String header,
    583             String footer) throws IOException {
    584         if (header.length() < 16 || !header.startsWith("-----BEGIN ") ||
    585                 !header.endsWith("-----")) {
    586             throw new IOException("Illegal header: " + header);
    587         }
    588         if (footer.length() < 14 || !footer.startsWith("-----END ") ||
    589                 !footer.endsWith("-----")) {
    590             throw new IOException("Illegal footer: " + footer);
    591         }
    592         String headerType = header.substring(11, header.length()-5);
    593         String footerType = footer.substring(9, footer.length()-5);
    594         if (!headerType.equals(footerType)) {
    595             throw new IOException("Header and footer do not match: " +
    596                     header + " " + footer);
    597         }
    598     }
    599 
    600     /**
    601      * Read one BER data block. This method is aware of indefinite-length BER
    602      * encoding and will read all of the sub-sections in a recursive way
    603      *
    604      * @param is    Read from this InputStream
    605      * @param bout  Write into this OutputStream
    606      * @param tag   Tag already read (-1 mean not read)
    607      * @returns     The current tag, used to check EOC in indefinite-length BER
    608      * @throws IOException Any parsing error
    609      */
    610     private static int readBERInternal(InputStream is,
    611             ByteArrayOutputStream bout, int tag) throws IOException {
    612 
    613         if (tag == -1) {        // Not read before the call, read now
    614             tag = is.read();
    615             if (tag == -1) {
    616                 throw new IOException("BER/DER tag info absent");
    617             }
    618             if ((tag & 0x1f) == 0x1f) {
    619                 throw new IOException("Multi octets tag not supported");
    620             }
    621             bout.write(tag);
    622         }
    623 
    624         int n = is.read();
    625         if (n == -1) {
    626             throw new IOException("BER/DER length info ansent");
    627         }
    628         bout.write(n);
    629 
    630         int length;
    631 
    632         if (n == 0x80) {        // Indefinite-length encoding
    633             if ((tag & 0x20) != 0x20) {
    634                 throw new IOException(
    635                         "Non constructed encoding must have definite length");
    636             }
    637             while (true) {
    638                 int subTag = readBERInternal(is, bout, -1);
    639                 if (subTag == 0) {   // EOC, end of indefinite-length section
    640                     break;
    641                 }
    642             }
    643         } else {
    644             if (n < 0x80) {
    645                 length = n;
    646             } else if (n == 0x81) {
    647                 length = is.read();
    648                 if (length == -1) {
    649                     throw new IOException("Incomplete BER/DER length info");
    650                 }
    651                 bout.write(length);
    652             } else if (n == 0x82) {
    653                 int highByte = is.read();
    654                 int lowByte = is.read();
    655                 if (lowByte == -1) {
    656                     throw new IOException("Incomplete BER/DER length info");
    657                 }
    658                 bout.write(highByte);
    659                 bout.write(lowByte);
    660                 length = (highByte << 8) | lowByte;
    661             } else if (n == 0x83) {
    662                 int highByte = is.read();
    663                 int midByte = is.read();
    664                 int lowByte = is.read();
    665                 if (lowByte == -1) {
    666                     throw new IOException("Incomplete BER/DER length info");
    667                 }
    668                 bout.write(highByte);
    669                 bout.write(midByte);
    670                 bout.write(lowByte);
    671                 length = (highByte << 16) | (midByte << 8) | lowByte;
    672             } else if (n == 0x84) {
    673                 int highByte = is.read();
    674                 int nextByte = is.read();
    675                 int midByte = is.read();
    676                 int lowByte = is.read();
    677                 if (lowByte == -1) {
    678                     throw new IOException("Incomplete BER/DER length info");
    679                 }
    680                 if (highByte > 127) {
    681                     throw new IOException("Invalid BER/DER data (a little huge?)");
    682                 }
    683                 bout.write(highByte);
    684                 bout.write(nextByte);
    685                 bout.write(midByte);
    686                 bout.write(lowByte);
    687                 length = (highByte << 24 ) | (nextByte << 16) |
    688                         (midByte << 8) | lowByte;
    689             } else { // ignore longer length forms
    690                 throw new IOException("Invalid BER/DER data (too huge?)");
    691             }
    692             if (readFully(is, bout, length) != length) {
    693                 throw new IOException("Incomplete BER/DER data");
    694             }
    695         }
    696         return tag;
    697     }
    698 }
    699