All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.bouncycastle.gpg.SExprParser Maven / Gradle / Ivy

Go to download

The Bouncy Castle Java APIs for the OpenPGP Protocol. The APIs are designed primarily to be used in conjunction with the BC FIPS provider. The APIs may also be used with other providers although if being used in a FIPS context it is the responsibility of the user to ensure that any other providers used are FIPS certified and used appropriately.

There is a newer version: 2.0.9
Show newest version
package org.bouncycastle.gpg;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.util.Date;

import org.bouncycastle.asn1.x9.ECNamedCurveTable;
import org.bouncycastle.bcpg.DSAPublicBCPGKey;
import org.bouncycastle.bcpg.DSASecretBCPGKey;
import org.bouncycastle.bcpg.ECDSAPublicBCPGKey;
import org.bouncycastle.bcpg.ECPublicBCPGKey;
import org.bouncycastle.bcpg.ECSecretBCPGKey;
import org.bouncycastle.bcpg.ElGamalPublicBCPGKey;
import org.bouncycastle.bcpg.ElGamalSecretBCPGKey;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.bouncycastle.bcpg.PublicKeyPacket;
import org.bouncycastle.bcpg.RSAPublicBCPGKey;
import org.bouncycastle.bcpg.RSASecretBCPGKey;
import org.bouncycastle.bcpg.S2K;
import org.bouncycastle.bcpg.SecretKeyPacket;
import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator;
import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.bouncycastle.openpgp.operator.PGPDigestCalculator;
import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.Strings;

/**
 * A parser for secret keys stored in SExpr
 */
public class SExprParser
{
    private final PGPDigestCalculatorProvider digestProvider;

    /**
     * Base constructor.
     *
     * @param digestProvider a provider for digest calculations. Used to confirm key protection hashes.
     */
    public SExprParser(PGPDigestCalculatorProvider digestProvider)
    {
        this.digestProvider = digestProvider;
    }

    /**
     * Parse a secret key from one of the GPG S expression keys associating it with the passed in public key.
     *
     * @return a secret key object.
     */
    public PGPSecretKey parseSecretKey(InputStream inputStream, PBEProtectionRemoverFactory keyProtectionRemoverFactory, PGPPublicKey pubKey)
        throws IOException, PGPException
    {
        SXprUtils.skipOpenParenthesis(inputStream);

        String type;

        type = SXprUtils.readString(inputStream, inputStream.read());
        if (type.equals("protected-private-key"))
        {
            SXprUtils.skipOpenParenthesis(inputStream);

            String keyType = SXprUtils.readString(inputStream, inputStream.read());
            if (keyType.equals("ecc"))
            {
                SXprUtils.skipOpenParenthesis(inputStream);

                String curveID = SXprUtils.readString(inputStream, inputStream.read());
                String curveName = SXprUtils.readString(inputStream, inputStream.read());

                SXprUtils.skipCloseParenthesis(inputStream);

                byte[] qVal;

                SXprUtils.skipOpenParenthesis(inputStream);

                type = SXprUtils.readString(inputStream, inputStream.read());
                if (type.equals("q"))
                {
                    qVal = SXprUtils.readBytes(inputStream, inputStream.read());
                }
                else
                {
                    throw new PGPException("no q value found");
                }

                SXprUtils.skipCloseParenthesis(inputStream);

                BigInteger d = processECSecretKey(inputStream, curveID, curveName, qVal, keyProtectionRemoverFactory);

                if (curveName.startsWith("NIST "))
                {
                    curveName = curveName.substring("NIST ".length());
                }

                ECPublicBCPGKey basePubKey = new ECDSAPublicBCPGKey(ECNamedCurveTable.getOID(curveName), new BigInteger(1, qVal));
                ECPublicBCPGKey assocPubKey = (ECPublicBCPGKey)pubKey.getPublicKeyPacket().getKey();
                if (!basePubKey.getCurveOID().equals(assocPubKey.getCurveOID())
                    || !basePubKey.getEncodedPoint().equals(assocPubKey.getEncodedPoint()))
                {
                    throw new PGPException("passed in public key does not match secret key");
                }

                return new PGPSecretKey(new SecretKeyPacket(pubKey.getPublicKeyPacket(), SymmetricKeyAlgorithmTags.NULL, null, null, new ECSecretBCPGKey(d).getEncoded()), pubKey);
            }
            else if (keyType.equals("dsa"))
            {
                BigInteger p = readBigInteger("p", inputStream);
                BigInteger q = readBigInteger("q", inputStream);
                BigInteger g = readBigInteger("g", inputStream);

                BigInteger y = readBigInteger("y", inputStream);

                BigInteger x = processDSASecretKey(inputStream, p, q, g, y, keyProtectionRemoverFactory);

                DSAPublicBCPGKey basePubKey = new DSAPublicBCPGKey(p, q, g, y);
                DSAPublicBCPGKey assocPubKey = (DSAPublicBCPGKey)pubKey.getPublicKeyPacket().getKey();
                if (!basePubKey.getP().equals(assocPubKey.getP())
                    || !basePubKey.getQ().equals(assocPubKey.getQ())
                    || !basePubKey.getG().equals(assocPubKey.getG())
                    || !basePubKey.getY().equals(assocPubKey.getY()))
                {
                    throw new PGPException("passed in public key does not match secret key");
                }
                return new PGPSecretKey(new SecretKeyPacket(pubKey.getPublicKeyPacket(), SymmetricKeyAlgorithmTags.NULL, null, null, new DSASecretBCPGKey(x).getEncoded()), pubKey);
            }
            else if (keyType.equals("elg"))
            {
                BigInteger p = readBigInteger("p", inputStream);
                BigInteger g = readBigInteger("g", inputStream);

                BigInteger y = readBigInteger("y", inputStream);

                BigInteger x = processElGamalSecretKey(inputStream, p, g, y, keyProtectionRemoverFactory);

                ElGamalPublicBCPGKey basePubKey = new ElGamalPublicBCPGKey(p, g, y);
                ElGamalPublicBCPGKey assocPubKey = (ElGamalPublicBCPGKey)pubKey.getPublicKeyPacket().getKey();
                if (!basePubKey.getP().equals(assocPubKey.getP())
                    || !basePubKey.getG().equals(assocPubKey.getG())
                    || !basePubKey.getY().equals(assocPubKey.getY()))
                {
                    throw new PGPException("passed in public key does not match secret key");
                }

                return new PGPSecretKey(new SecretKeyPacket(pubKey.getPublicKeyPacket(), SymmetricKeyAlgorithmTags.NULL, null, null, new ElGamalSecretBCPGKey(x).getEncoded()), pubKey);
            }
            else if (keyType.equals("rsa"))
            {
                BigInteger n = readBigInteger("n", inputStream);
                BigInteger e = readBigInteger("e", inputStream);

                BigInteger[] values = processRSASecretKey(inputStream, n, e, keyProtectionRemoverFactory);

                // TODO: type of RSA key?
                RSAPublicBCPGKey basePubKey = new RSAPublicBCPGKey(n, e);
                RSAPublicBCPGKey assocPubKey = (RSAPublicBCPGKey)pubKey.getPublicKeyPacket().getKey();
                if (!basePubKey.getModulus().equals(assocPubKey.getModulus())
                    || !basePubKey.getPublicExponent().equals(assocPubKey.getPublicExponent()))
                {
                    throw new PGPException("passed in public key does not match secret key");
                }

                return new PGPSecretKey(new SecretKeyPacket(pubKey.getPublicKeyPacket(), SymmetricKeyAlgorithmTags.NULL, null, null, new RSASecretBCPGKey(values[0], values[1], values[2]).getEncoded()), pubKey);
            }
            else
            {
                throw new PGPException("unknown key type: " + keyType);
            }
        }

        throw new PGPException("unknown key type found");
    }

    /**
     * Parse a secret key from one of the GPG S expression keys.
     *
     * @return a secret key object.
     */
    public PGPSecretKey parseSecretKey(InputStream inputStream, PBEProtectionRemoverFactory keyProtectionRemoverFactory, KeyFingerPrintCalculator fingerPrintCalculator)
        throws IOException, PGPException
    {
        SXprUtils.skipOpenParenthesis(inputStream);

        String type;

        type = SXprUtils.readString(inputStream, inputStream.read());
        if (type.equals("protected-private-key"))
        {
            SXprUtils.skipOpenParenthesis(inputStream);

            String keyType = SXprUtils.readString(inputStream, inputStream.read());
            if (keyType.equals("ecc"))
            {
                SXprUtils.skipOpenParenthesis(inputStream);

                String curveID = SXprUtils.readString(inputStream, inputStream.read());
                String curveName = SXprUtils.readString(inputStream, inputStream.read());

                if (curveName.startsWith("NIST "))
                {
                    curveName = curveName.substring("NIST ".length());
                }

                SXprUtils.skipCloseParenthesis(inputStream);

                byte[] qVal;

                SXprUtils.skipOpenParenthesis(inputStream);

                type = SXprUtils.readString(inputStream, inputStream.read());
                if (type.equals("q"))
                {
                    qVal = SXprUtils.readBytes(inputStream, inputStream.read());
                }
                else
                {
                    throw new PGPException("no q value found");
                }

                PublicKeyPacket pubPacket = new PublicKeyPacket(PublicKeyAlgorithmTags.ECDSA, new Date(), new ECDSAPublicBCPGKey(ECNamedCurveTable.getOID(curveName), new BigInteger(1, qVal)));

                SXprUtils.skipCloseParenthesis(inputStream);

                BigInteger d = processECSecretKey(inputStream, curveID, curveName, qVal, keyProtectionRemoverFactory);

                return new PGPSecretKey(new SecretKeyPacket(pubPacket, SymmetricKeyAlgorithmTags.NULL, null, null, new ECSecretBCPGKey(d).getEncoded()), new PGPPublicKey(pubPacket, fingerPrintCalculator));
            }
            else if (keyType.equals("dsa"))
            {
                BigInteger p = readBigInteger("p", inputStream);
                BigInteger q = readBigInteger("q", inputStream);
                BigInteger g = readBigInteger("g", inputStream);

                BigInteger y = readBigInteger("y", inputStream);

                BigInteger x = processDSASecretKey(inputStream, p, q, g, y, keyProtectionRemoverFactory);

                PublicKeyPacket pubPacket = new PublicKeyPacket(PublicKeyAlgorithmTags.DSA, new Date(), new DSAPublicBCPGKey(p, q, g, y));

                return new PGPSecretKey(new SecretKeyPacket(pubPacket, SymmetricKeyAlgorithmTags.NULL, null, null, new DSASecretBCPGKey(x).getEncoded()), new PGPPublicKey(pubPacket, fingerPrintCalculator));
            }
            else if (keyType.equals("elg"))
            {
                BigInteger p = readBigInteger("p", inputStream);
                BigInteger g = readBigInteger("g", inputStream);

                BigInteger y = readBigInteger("y", inputStream);

                BigInteger x = processElGamalSecretKey(inputStream, p, g, y, keyProtectionRemoverFactory);

                PublicKeyPacket pubPacket = new PublicKeyPacket(PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT, new Date(), new ElGamalPublicBCPGKey(p, g, y));

                return new PGPSecretKey(new SecretKeyPacket(pubPacket, SymmetricKeyAlgorithmTags.NULL, null, null, new ElGamalSecretBCPGKey(x).getEncoded()), new PGPPublicKey(pubPacket, fingerPrintCalculator));
            }
            else if (keyType.equals("rsa"))
            {
                BigInteger n = readBigInteger("n", inputStream);
                BigInteger e = readBigInteger("e", inputStream);

                BigInteger[] values = processRSASecretKey(inputStream, n, e, keyProtectionRemoverFactory);

                // TODO: type of RSA key?
                PublicKeyPacket pubPacket = new PublicKeyPacket(PublicKeyAlgorithmTags.RSA_GENERAL, new Date(), new RSAPublicBCPGKey(n, e));

                return new PGPSecretKey(new SecretKeyPacket(pubPacket, SymmetricKeyAlgorithmTags.NULL, null, null, new RSASecretBCPGKey(values[0], values[1], values[2]).getEncoded()), new PGPPublicKey(pubPacket, fingerPrintCalculator));
            }
            else
            {
                throw new PGPException("unknown key type: " + keyType);
            }
        }

        throw new PGPException("unknown key type found");
    }

    private BigInteger readBigInteger(String expectedType, InputStream inputStream)
        throws IOException, PGPException
    {
        SXprUtils.skipOpenParenthesis(inputStream);

        String type = SXprUtils.readString(inputStream, inputStream.read());
        if (!type.equals(expectedType))
        {
            throw new PGPException(expectedType + " value expected");
        }

        byte[] nBytes = SXprUtils.readBytes(inputStream, inputStream.read());
        BigInteger v = new BigInteger(1, nBytes);

        SXprUtils.skipCloseParenthesis(inputStream);

        return v;
    }

    private static byte[][] extractData(InputStream inputStream, PBEProtectionRemoverFactory keyProtectionRemoverFactory)
        throws PGPException, IOException
    {
        byte[] data;
        byte[] protectedAt = null;

        SXprUtils.skipOpenParenthesis(inputStream);

        String type = SXprUtils.readString(inputStream, inputStream.read());
        if (type.equals("protected"))
        {
            String protection = SXprUtils.readString(inputStream, inputStream.read());

            SXprUtils.skipOpenParenthesis(inputStream);

            S2K s2k = SXprUtils.parseS2K(inputStream);

            byte[] iv = SXprUtils.readBytes(inputStream, inputStream.read());

            SXprUtils.skipCloseParenthesis(inputStream);

            byte[] secKeyData = SXprUtils.readBytes(inputStream, inputStream.read());

            SXprUtils.skipCloseParenthesis(inputStream);

            PBESecretKeyDecryptor keyDecryptor = keyProtectionRemoverFactory.createDecryptor(protection);

            // TODO: recognise other algorithms
            byte[] key = keyDecryptor.makeKeyFromPassPhrase(SymmetricKeyAlgorithmTags.AES_128, s2k);

            data = keyDecryptor.recoverKeyData(SymmetricKeyAlgorithmTags.AES_128, key, iv, secKeyData, 0, secKeyData.length);

            // check if protected at is present
            if (inputStream.read() == '(')
            {
                ByteArrayOutputStream bOut = new ByteArrayOutputStream();

                bOut.write('(');
                int ch;
                while ((ch = inputStream.read()) >= 0 && ch != ')')
                {
                    bOut.write(ch);
                }

                if (ch != ')')
                {
                    throw new IOException("unexpected end to SExpr");
                }

                bOut.write(')');

                protectedAt = bOut.toByteArray();
            }

            SXprUtils.skipCloseParenthesis(inputStream);
            SXprUtils.skipCloseParenthesis(inputStream);
        }
        else
        {
            throw new PGPException("protected block not found");
        }

        return new byte[][]{data, protectedAt};
    }

    private BigInteger processDSASecretKey(InputStream inputStream, BigInteger p, BigInteger q, BigInteger g, BigInteger y,
                                                    PBEProtectionRemoverFactory keyProtectionRemoverFactory)
        throws IOException, PGPException
    {
        String type;
        byte[][] basicData = extractData(inputStream, keyProtectionRemoverFactory);

        byte[] keyData = basicData[0];
        byte[] protectedAt = basicData[1];

        //
        // parse the secret key S-expr
        //
        InputStream keyIn = new ByteArrayInputStream(keyData);

        SXprUtils.skipOpenParenthesis(keyIn);
        SXprUtils.skipOpenParenthesis(keyIn);

        BigInteger x = readBigInteger("x", keyIn);

        SXprUtils.skipCloseParenthesis(keyIn);

        SXprUtils.skipOpenParenthesis(keyIn);
        type = SXprUtils.readString(keyIn, keyIn.read());

        if (!type.equals("hash"))
        {
            throw new PGPException("hash keyword expected");
        }
        type = SXprUtils.readString(keyIn, keyIn.read());

        if (!type.equals("sha1"))
        {
            throw new PGPException("hash keyword expected");
        }

        byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());

        SXprUtils.skipCloseParenthesis(keyIn);

        if (digestProvider != null)
        {
            PGPDigestCalculator digestCalculator = digestProvider.get(HashAlgorithmTags.SHA1);

            OutputStream dOut = digestCalculator.getOutputStream();

            dOut.write(Strings.toByteArray("(3:dsa"));
            writeCanonical(dOut, "p", p);
            writeCanonical(dOut, "q", q);
            writeCanonical(dOut, "g", g);
            writeCanonical(dOut, "y", y);
            writeCanonical(dOut, "x", x);

            // check protected-at
            if (protectedAt != null)
            {
                dOut.write(protectedAt);
            }

            dOut.write(Strings.toByteArray(")"));

            byte[] check = digestCalculator.getDigest();
            if (!Arrays.constantTimeAreEqual(check, hashBytes))
            {
                throw new PGPException("checksum on protected data failed in SExpr");
            }
        }

        return x;
    }

    private BigInteger processElGamalSecretKey(InputStream inputStream, BigInteger p, BigInteger g, BigInteger y,
                                                    PBEProtectionRemoverFactory keyProtectionRemoverFactory)
        throws IOException, PGPException
    {
        String type;
        byte[][] basicData = extractData(inputStream, keyProtectionRemoverFactory);

        byte[] keyData = basicData[0];
        byte[] protectedAt = basicData[1];

        //
        // parse the secret key S-expr
        //
        InputStream keyIn = new ByteArrayInputStream(keyData);

        SXprUtils.skipOpenParenthesis(keyIn);
        SXprUtils.skipOpenParenthesis(keyIn);

        BigInteger x = readBigInteger("x", keyIn);

        SXprUtils.skipCloseParenthesis(keyIn);

        SXprUtils.skipOpenParenthesis(keyIn);
        type = SXprUtils.readString(keyIn, keyIn.read());

        if (!type.equals("hash"))
        {
            throw new PGPException("hash keyword expected");
        }
        type = SXprUtils.readString(keyIn, keyIn.read());

        if (!type.equals("sha1"))
        {
            throw new PGPException("hash keyword expected");
        }

        byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());

        SXprUtils.skipCloseParenthesis(keyIn);

        if (digestProvider != null)
        {
            PGPDigestCalculator digestCalculator = digestProvider.get(HashAlgorithmTags.SHA1);

            OutputStream dOut = digestCalculator.getOutputStream();

            dOut.write(Strings.toByteArray("(3:elg"));
            writeCanonical(dOut, "p", p);
            writeCanonical(dOut, "g", g);
            writeCanonical(dOut, "y", y);
            writeCanonical(dOut, "x", x);

            // check protected-at
            if (protectedAt != null)
            {
                dOut.write(protectedAt);
            }

            dOut.write(Strings.toByteArray(")"));

            byte[] check = digestCalculator.getDigest();
            if (!Arrays.constantTimeAreEqual(check, hashBytes))
            {
                throw new PGPException("checksum on protected data failed in SExpr");
            }
        }

        return x;
    }

    private BigInteger processECSecretKey(InputStream inputStream, String curveID, String curveName, byte[] qVal,
                                             PBEProtectionRemoverFactory keyProtectionRemoverFactory)
        throws IOException, PGPException
    {
        String type;

        byte[][] basicData = extractData(inputStream, keyProtectionRemoverFactory);

        byte[] keyData = basicData[0];
        byte[] protectedAt = basicData[1];

        //
        // parse the secret key S-expr
        //
        InputStream keyIn = new ByteArrayInputStream(keyData);

        SXprUtils.skipOpenParenthesis(keyIn);
        SXprUtils.skipOpenParenthesis(keyIn);
        BigInteger d = readBigInteger("d", keyIn);
        SXprUtils.skipCloseParenthesis(keyIn);

        SXprUtils.skipOpenParenthesis(keyIn);

        type = SXprUtils.readString(keyIn, keyIn.read());

        if (!type.equals("hash"))
        {
            throw new PGPException("hash keyword expected");
        }
        type = SXprUtils.readString(keyIn, keyIn.read());

        if (!type.equals("sha1"))
        {
            throw new PGPException("hash keyword expected");
        }

        byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());

        SXprUtils.skipCloseParenthesis(keyIn);

        if (digestProvider != null)
        {
            PGPDigestCalculator digestCalculator = digestProvider.get(HashAlgorithmTags.SHA1);

            OutputStream dOut = digestCalculator.getOutputStream();

            dOut.write(Strings.toByteArray("(3:ecc"));

            dOut.write(Strings.toByteArray("(" + curveID.length() + ":" + curveID + curveName.length() + ":" + curveName + ")"));

            writeCanonical(dOut, "q", qVal);
            writeCanonical(dOut, "d", d);

            // check protected-at
            if (protectedAt != null)
            {
                dOut.write(protectedAt);
            }

            dOut.write(Strings.toByteArray(")"));

            byte[] check = digestCalculator.getDigest();

            if (!Arrays.constantTimeAreEqual(check, hashBytes))
            {
                throw new PGPException("checksum on protected data failed in SExpr");
            }
        }

        return d;
    }

    private BigInteger[] processRSASecretKey(InputStream inputStream, BigInteger n, BigInteger e,
                                                    PBEProtectionRemoverFactory keyProtectionRemoverFactory)
        throws IOException, PGPException
    {
        String type;
        byte[][] basicData = extractData(inputStream, keyProtectionRemoverFactory);

        byte[] keyData = basicData[0];
        byte[] protectedAt = basicData[1];

        //
        // parse the secret key S-expr
        //
        InputStream keyIn = new ByteArrayInputStream(keyData);

        SXprUtils.skipOpenParenthesis(keyIn);
        SXprUtils.skipOpenParenthesis(keyIn);

        BigInteger d = readBigInteger("d", keyIn);
        BigInteger p = readBigInteger("p", keyIn);
        BigInteger q = readBigInteger("q", keyIn);
        BigInteger u = readBigInteger("u", keyIn);

        SXprUtils.skipCloseParenthesis(keyIn);

        SXprUtils.skipOpenParenthesis(keyIn);
        type = SXprUtils.readString(keyIn, keyIn.read());

        if (!type.equals("hash"))
        {
            throw new PGPException("hash keyword expected");
        }
        type = SXprUtils.readString(keyIn, keyIn.read());

        if (!type.equals("sha1"))
        {
            throw new PGPException("hash keyword expected");
        }

        byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());

        SXprUtils.skipCloseParenthesis(keyIn);

        if (digestProvider != null)
        {
            PGPDigestCalculator digestCalculator = digestProvider.get(HashAlgorithmTags.SHA1);

            OutputStream dOut = digestCalculator.getOutputStream();

            dOut.write(Strings.toByteArray("(3:rsa"));

            writeCanonical(dOut, "n", n);
            writeCanonical(dOut, "e", e);
            writeCanonical(dOut, "d", d);
            writeCanonical(dOut, "p", p);
            writeCanonical(dOut, "q", q);
            writeCanonical(dOut, "u", u);

            // check protected-at
            if (protectedAt != null)
            {
                dOut.write(protectedAt);
            }

            dOut.write(Strings.toByteArray(")"));

            byte[] check = digestCalculator.getDigest();

            if (!Arrays.constantTimeAreEqual(check, hashBytes))
            {
                throw new PGPException("checksum on protected data failed in SExpr");
            }
        }

        return new BigInteger[]{d, p, q, u};
    }

    private void writeCanonical(OutputStream dOut, String label, BigInteger i)
        throws IOException
    {
        writeCanonical(dOut, label, i.toByteArray());
    }
    
    private void writeCanonical(OutputStream dOut, String label, byte[] data)
        throws IOException
    {
        dOut.write(Strings.toByteArray("(" + label.length() + ":" + label + data.length + ":"));
        dOut.write(data);
        dOut.write(Strings.toByteArray(")"));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy