uk.ac.ox.webauth.Token.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.ox.webauth.Token.java

Source

/*
 * Webauth Java - Java implementation of the University of Stanford WebAuth
 * protocol.
 *
 * Copyright (C) 2006 University of Oxford
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package uk.ac.ox.webauth;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;

import static javax.crypto.Cipher.DECRYPT_MODE;
import static javax.crypto.Cipher.ENCRYPT_MODE;

/**
 * Codec for Webauth tokens.
 *
 * <p/>$HeadURL$
 * <br/>$LastChangedRevision$
 * <br/>$LastChangedDate$
 * <br/>$LastChangedBy$
 * @author     Mats Henrikson
 * @version    $LastChangedRevision$
 */
public class Token {

    /** A list of key-value pairs to add when encoding the token. */
    private final Map<String, KeyValuePair> kv = new HashMap<String, KeyValuePair>();
    /** A Stringifier to use to return a string representing this token. */
    private Stringifier stringifier;

    /** @return false if the token is invalid. */
    public boolean valid() {
        return valid;
    }

    private boolean valid = false;

    /** Somewhere to get random padding bytes from. */
    private static final Random RAND = new Random();
    /** A blank initialisation vector to use since the nonce is used as IV. */
    private static final IvParameterSpec IV = new IvParameterSpec(
            new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });
    /** A byte holding the value of ; in US-ASCII. */
    private static final byte SEMI_COLON;
    /** A byte holding the value of = in US-ASCII. */
    private static final byte EQUALS;

    static {
        byte semiColon = 0;
        byte equals = 0;
        try {
            semiColon = ";".getBytes("US-ASCII")[0];
            equals = "=".getBytes("US-ASCII")[0];
        } catch (UnsupportedEncodingException uee) {
            /* US-ASCII should always exist. */
            uee.printStackTrace();
        }
        // trick the compiler into not showing 'variable might not have been initialised' errors
        SEMI_COLON = semiColon;
        EQUALS = equals;
    }

    /** Test encoding/decoding a token. */
    public static void main(String[] args) throws Exception {
        // decrypt a base64 encoded token given in args[0] with a key given in args[1]
        if (args[0] != null && args[1] != null) {
            WebauthKey secretKey = new WebauthKey(0, 0, 0, 0, args[1]);
            Token t = new Token(Base64.decodeBase64(args[0].getBytes("US-ASCII")), secretKey.key());
            System.out.println(t.toString());
            System.exit(0);
        }

        byte[] keyBytes = new byte[16];
        RAND.nextBytes(keyBytes);
        WebauthKey secretKey = new WebauthKey(0, 0, 0, 0, new String(Hex.encodeHex(keyBytes)));
        //SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");

        String key1 = "key1";
        String key2 = "key2";
        String key3 = "key3";
        String key4 = "key4";

        String data1 = "data1";
        String data2 = "data;2";
        byte[] data3 = { 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74 };
        byte[] data4 = { 64, SEMI_COLON, 65, 66, 67, 68, 69, 70, 71, 72, 73, SEMI_COLON, 74 };

        Token token1 = new Token();
        token1.add(key1, data1);
        token1.add(key2, data2);
        token1.add(key3, data3);
        token1.add(key4, data4);
        String encrypted1 = token1.encrypt(secretKey.key());

        Token token2 = new Token();
        token2.add(key1, data1);
        token2.add(key2, data2);
        token2.add(key3, data3);
        token2.add(key4, data4);
        String encrypted2 = token2.encrypt(secretKey.key());
        if (encrypted1.equals(encrypted2)) {
            System.err.println("Two equal tokens encrypted and encoded into the same value, this is broken!");
            System.exit(1);
        } else {
            System.out.println("Two tokens with the same data do not encrypt to the same value, good.");
        }

        Token token3 = new Token(Base64.decodeBase64(encrypted1.getBytes("US-ASCII")), secretKey.key());
        boolean broken = false;
        if (!data1.equals(token3.getString(key1))) {
            System.err.println("data1 is different (or missing) after decryption.");
            broken = true;
        }
        if (!data2.equals(token3.getString(key2))) {
            System.err.println("data2 is different (or missing) after decryption.");
            broken = true;
        }
        if (!Arrays.equals(data3, token3.getBinary(key3))) {
            System.err.println("data3 is different (or missing) after decryption.");
            broken = true;
        }
        if (!Arrays.equals(data4, token3.getBinary(key4))) {
            System.err.println("data4 is different (or missing) after decryption.");
            broken = true;
        }

        if (broken) {
            System.err.println("Encryption/decryption is broken.");
            System.exit(1);
        } else {
            System.out.println("Encryption/decryption works.");
        }
    }

    /** Do nothing constructor. */
    public Token() {
        // XXX: this might break printing of webauth tokens
        // create the Stringifier to use
        stringifier = new KerberosTokenStringifier();
    }

    /**
     * Special constructor for a krb5 credential data token.
     * @param   data    The byte array holding the key value pairs.
     */
    public Token(byte[] data) {
        // create all the key-value pairs
        for (int i = 0, start = 0; (i = indexOf(SEMI_COLON, data, i)) != -1;) {
            i++;
            if (i < data.length && data[i] == SEMI_COLON) {
                i++;
                continue;
            }
            byte[] keyValuePairArray = new byte[i - start];
            System.arraycopy(data, start, keyValuePairArray, 0, keyValuePairArray.length);
            KeyValuePair kvp = new KeyValuePair(keyValuePairArray);
            try {
                kv.put(new String(kvp.key(), "US-ASCII"), kvp);
            } catch (UnsupportedEncodingException uee) {
                /* should never happen as US-ASCII must exist */
                uee.printStackTrace();
            }
            start = i;
        }
        valid = true;

        // create the Stringifier to use
        stringifier = new KerberosTokenStringifier();
    }

    /**
     * Initialise a token with a base64 encoded Webauth token.
     * @param   tokenData   The data to be decrypted.
     * @param   sessionKey  The session key to use for the AES and Hmac.
     * @throws  GeneralSecurityException    if there was a problem with the security code used.
     */
    public Token(byte[] tokenData, Key sessionKey) throws GeneralSecurityException {
        // a token is:
        // {key-hint}{nonce   }{hmac    }{token-attributes     }{padding         }
        // {4 bytes }{16 bytes}{20 bytes}{make the data into multiple of 16 bytes}
        // everything after the key hint is aes encrypted

        try {
            // set up some streams
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(tokenData);
            DataInputStream dataInputStream = new DataInputStream(byteArrayInputStream);

            // read the key hint
            int keyHint = dataInputStream.readInt();

            // prepare to AES decrypt the rest
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            cipher.init(DECRYPT_MODE, sessionKey, IV);
            CipherInputStream decrypt = new CipherInputStream(byteArrayInputStream, cipher);

            // throw away the nonce
            if (decrypt.read(new byte[16]) != 16) {
                throw new GeneralSecurityException("Failed to read nonce from token.");
            }

            // read the HMACSHA1 checksum
            byte[] checksum = new byte[20];
            if (decrypt.read(checksum) != 20) {
                throw new GeneralSecurityException("Failed to read HMAC SHA1 checksum from token.");
            }

            // read in the rest of the data
            ByteArrayOutputStream tokenByteArrayOutputStream = new ByteArrayOutputStream();
            for (int b = decrypt.read(); b != -1; b = decrypt.read()) {
                tokenByteArrayOutputStream.write(b);
            }
            byte[] data = tokenByteArrayOutputStream.toByteArray();
            decrypt.close();

            // check the hmacsha1
            Mac hmacSHA1 = Mac.getInstance("HmacSHA1");
            hmacSHA1.init(sessionKey);
            if (!Arrays.equals(checksum, hmacSHA1.doFinal(data))) {
                throw new GeneralSecurityException("Invalid token, checksum mismatch.");
            }

            // create all the key-value pairs
            for (int i = 0, start = 0; (i = indexOf(SEMI_COLON, data, i)) != -1;) {
                i++;
                if (i < data.length && data[i] == SEMI_COLON) {
                    i++;
                    continue;
                }
                byte[] keyValuePairArray = new byte[i - start];
                System.arraycopy(data, start, keyValuePairArray, 0, keyValuePairArray.length);
                KeyValuePair kvp = new KeyValuePair(keyValuePairArray);
                kv.put(new String(kvp.key(), "US-ASCII"), kvp);
                start = i;
            }
        } catch (IOException ioe) {
            /* should never happen as it's a ByteArrayInputStream */
            ioe.printStackTrace();
        }
        valid = true;

        // create the Stringifier to use
        stringifier = new WebauthTokenStringifier();
    }

    /**
     * Add a key and value pair. No need to do any escaping, it's done
     * automagically. The strings must be in US-ASCII otherwise the result is
     * undefined.
     * @param   key     The name of the key for this value.
     * @param   value   The value.
     */
    public void add(String key, String value) {
        try {
            kv.put(key, new KeyValuePair(key.getBytes("US-ASCII"), value.getBytes("US-ASCII")));
        } catch (UnsupportedEncodingException uee) {
            /* should never happen as US-ASCII must exist in a Java impl. */
            uee.printStackTrace();
        }
    }

    /**
     * Add a key and value pair. No need to do any escaping, it's done
     * automagically. The strings must be in US-ASCII otherwise the result is
     * undefined.
     * @param   key     The name of the key for this value.
     * @param   value   The byte array that holds the binary value.
     */
    public void add(String key, byte[] value) {
        try {
            kv.put(key, new KeyValuePair(key.getBytes("US-ASCII"), value));
        } catch (UnsupportedEncodingException uee) {
            /* should never happen as US-ASCII must exist in a Java impl. */
            uee.printStackTrace();
        }
    }

    /**
     * Return all the key names.
     * @return  a set with all the key names.
     */
    public Set<String> keySet() {
        return Collections.unmodifiableSet(kv.keySet());
    }

    /**
     * Return the corresponding String for a key, if present in the token. This
     * is a relatively expensive operation.
     * @param   key The key to look for a value fur.
     * @return  the value as a String, or null if not available. Note that if
     *          the value is actually some binary data the result will be undefined.
     */
    public String getString(String key) {
        byte[] data = getBinary(key);
        try {
            return (data == null) ? null : new String(data, "US-ASCII");
        } catch (UnsupportedEncodingException uee) {
            /* should never happen as US-ASCII must exist in a Java impl. */
            uee.printStackTrace();
        }
        return null; // since there must always be a return
    }

    /**
     * Return the corresponding array of binary data for a key, if present in
     * the token. This is a relatively expensive operation.
     * @param   key The key to look for a value fur.
     * @return  the value as an array of bytes.
     */
    public byte[] getBinary(String key) {
        KeyValuePair kvp = kv.get(key);
        return (kvp == null) ? null : kvp.value();
    }

    /**
     * Encode the token and return it.
     * @param   sessionKey  The session key to use to AES encrypt and feed the HMAC.
     * @return  The escaped, encrypted and base64 encoded token.
     * @throws  GeneralSecurityException    if there was a problem with the security code used.
     */
    public String encrypt(Key sessionKey) throws GeneralSecurityException {
        // a token is:
        // {key-hint}{nonce   }{hmac    }{token-attributes     }{padding         }
        // {4 bytes }{16 bytes}{20 bytes}{make the data into multiple of 16 bytes}
        // everything after the key hint is aes encrypted

        // this is where we want to final data packet to end up
        ByteArrayOutputStream data = new ByteArrayOutputStream();
        try {
            data.write(unixTimestampBytes(System.currentTimeMillis()));

            // set up the AES encryption
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            cipher.init(ENCRYPT_MODE, sessionKey, IV);
            CipherOutputStream encrypt = new CipherOutputStream(data, cipher);

            // write the nonce
            byte[] nonce = new byte[16];
            RAND.nextBytes(nonce);
            encrypt.write(nonce);

            // put together the actual key-value pair data to send
            ByteArrayOutputStream paddedKeyValueData = new ByteArrayOutputStream();
            for (KeyValuePair kvp : kv.values()) {
                paddedKeyValueData.write(kvp.bytes());
            }

            // and pad it (including the size of the hmac to be added later)
            int padding = 16 - ((20 + paddedKeyValueData.size()) % 16);
            for (int i = 0; i < padding; i++) {
                paddedKeyValueData.write(padding);
            }
            byte[] paddedKeyValueDataArray = paddedKeyValueData.toByteArray();

            // then work out and write the SHA1 HMAC
            Mac hmacSHA1 = Mac.getInstance("HmacSHA1");
            hmacSHA1.init(sessionKey);
            encrypt.write(hmacSHA1.doFinal(paddedKeyValueDataArray));

            // then write the actual key-value pair data and padding and close it
            encrypt.write(paddedKeyValueDataArray);
            encrypt.close();
        } catch (IOException ioe) {
            /* should never happen as it's a ByteArrayOutputStream */
            ioe.printStackTrace();
        }

        // return the token after base64 encoding it
        return new String(Base64.encodeBase64(data.toByteArray()));
    }

    /**
     * Convenience method for getting a byte array with a unix timestamp, i.e.
     * number of seconds since the epoch.
     * @param   date    The date to base the timestamp on.
     * @return  A unix time stamp in an integer saved in network byte order in
     *          a byte array.
     */
    public static byte[] unixTimestampBytes(Date date) {
        return unixTimestampBytes(date.getTime());
    }

    /**
     * Convenience method for getting a byte array with a unix timestamp, i.e.
     * number of seconds since the epoch.
     * @param   date    The date to base the timestamp on, such as System.currentTimeMillis().
     * @return  A unix time stamp in an integer saved in network byte order in
     *          a byte array.
     */
    public static byte[] unixTimestampBytes(long date) {
        ByteArrayOutputStream arrayStream = new ByteArrayOutputStream();
        try {
            DataOutputStream dataStream = new DataOutputStream(arrayStream);
            dataStream.writeInt((int) (date / 1000L));
        } catch (IOException ioe) {
            /* should never happen as it's a ByteArrayOutputStream */
            ioe.printStackTrace();
        }
        return arrayStream.toByteArray();
    }

    /**
     * Turn a byte array into an int.
     * @param   bytes   The bytes to turn into an int.
     * @return  An Integer number, or null if there is a problem.
     */
    public static Integer bytesToInt(byte[] bytes) {
        if (!(bytes.length == 4)) {
            return null;
        }
        DataInputStream in = new DataInputStream(new ByteArrayInputStream(bytes));
        try {
            return in.readInt();
        } catch (IOException ioe) {
            ioe.printStackTrace();
            return null;
        }
    }

    /**
     * Turn a byte array with a unix timestamp into a Date.
     * @param   bytes   A byte array of length 4 containing an int representing
     *          a unix timestamp, i.e. number of seconds since the epoch.
     * @return  a Date representing the unix timestamp, or null if the timestamp
     *          is somehow invalid.
     */
    public static Date bytesToDate(byte[] bytes) {
        Integer n = bytesToInt(bytes);
        if (n == null) {
            return null;
        }
        return new Date(n * 1000L);
    }

    /**
     * Return the index of a byte in an array.
     * @param   b       The byte to search for in the array.
     * @param   data    The byte array to search in.
     * @param   offset  The index (inclusive) to start the search at.
     * @return  the index where the byte was found, or -1 if it wasn't found.
     */
    private static int indexOf(byte b, byte[] data, int offset) {
        int index = -1;
        for (int i = offset; i < data.length; i++) {
            if (data[i] == b) {
                index = i;
                break;
            }
        }
        return index;
    }

    /**
     * Escape all ; in the data.
     * @param   data    The data to escape.
     * @return  the escaped data.
     */
    private static byte[] escape(byte[] data) {
        ByteArrayOutputStream escaped = new ByteArrayOutputStream();
        for (int i = 0; i < data.length; i++) {
            if (data[i] == SEMI_COLON) {
                escaped.write(SEMI_COLON);
            }
            escaped.write(data[i]);
        }
        return escaped.toByteArray();
    }

    /**
     * Unescape all ;; in the data, and chop off the last ;.
     * @param   data    The data to unescape.
     * @param   offset  Start at this index in the data array;
     * @return  the unescaped data.
     */
    private static byte[] unescape(byte[] data, int offset) {
        ByteArrayOutputStream unescaped = new ByteArrayOutputStream();
        int dataLength = data.length - 1; // don't read the last ;
        for (int i = offset; i < dataLength; i++) {
            unescaped.write(data[i]);
            if (data[i] == SEMI_COLON) {
                i++;
            }
        }
        return unescaped.toByteArray();
    }

    /**
     * Return a String describing this token.
     * @return  a string describing the token.
     */
    public String toString() {
        return stringifier.toString(this);
    }

    /** A class to delegate key and value pairs to. */
    private static class KeyValuePair {

        /** The key bytes. */
        public byte[] key() {
            return key;
        }

        private byte[] key;

        /** The value bytes. */
        public byte[] value() {
            return value;
        }

        private byte[] value;

        /**
         * Pass in an escaped key-value pair byte array and it parses it into a
         * key and value.
         * @param   data    An escaped array with the key and the value, terminated 
         *          with a semicolon.
         */
        public KeyValuePair(byte[] data) {
            int equalsIndex = indexOf(EQUALS, data, 0);
            key = new byte[equalsIndex];
            System.arraycopy(data, 0, key, 0, equalsIndex);
            value = unescape(data, equalsIndex + 1);
        }

        /**
         * Take the data and escape all ; in it.
         * @param   key     The key for the data.
         * @param   value   The bytes for the value.
         */
        public KeyValuePair(byte[] key, byte[] value) {
            this.key = key;
            this.value = value;
        }

        /**
         * Return the escaped bytes for this key-value pair.
         * @return  the escaped bytes.
         */
        public byte[] bytes() {
            ByteArrayOutputStream escaped = new ByteArrayOutputStream();
            try {
                escaped.write(key);
                escaped.write(EQUALS);
                escaped.write(escape(value));
                escaped.write(SEMI_COLON);
            } catch (IOException ioe) {
                /* should never happen as it's a ByteArrayOutputStream */
                ioe.printStackTrace();
            }
            return escaped.toByteArray();
        }
    }

    /** Interface to make the objects that turn a token into a string. */
    private static interface Stringifier {
        /** Turn a token into a string. */
        public String toString(Token t);
    }

    /** Class to stringify a regular Webauth token. */
    private static class WebauthTokenStringifier implements Stringifier {

        /** An unmodifiable Set holding all names of tokens that are Strings. */
        private static final Set STRING_TOKENS;
        /** An unmodifiable Set holding all names of tokens that are arrays of binary data. */
        private static final Set BINARY_TOKENS;
        /** An unmodifiable Set holding all names of tokens that are arrays of binary data representing a timestamp. */
        private static final Set DATE_TOKENS;

        static {
            // initialise things for the toString() method
            String[] stringTokens = { "cmd", "crs", "crt", "ec", "em", "p", "ps", "pt", "ro", "rtt", "ru", "s",
                    "sa", "t", "u" };
            Set<String> strings = new TreeSet<String>();
            for (String s : stringTokens) {
                strings.add(s);
            }
            STRING_TOKENS = Collections.unmodifiableSet(strings);

            String[] binaryTokens = { "as", "crd", "k", "pd", "sad", "wt" };
            Set<String> binary = new TreeSet<String>();
            for (String s : binaryTokens) {
                binary.add(s);
            }
            BINARY_TOKENS = Collections.unmodifiableSet(binary);

            String[] dateTokens = { "ct", "et", "lt" };
            Set<String> dates = new TreeSet<String>();
            for (String s : dateTokens) {
                dates.add(s);
            }
            DATE_TOKENS = Collections.unmodifiableSet(dates);
        }

        public String toString(Token t) {
            StringBuilder string = new StringBuilder();
            string.append("Webauth token keys and values:\n");
            for (String key : t.kv.keySet()) {
                string.append(key).append(": ");
                if (STRING_TOKENS.contains(key)) {
                    string.append("'").append(t.getString(key)).append("'");
                } else if (BINARY_TOKENS.contains(key)) {
                    string.append("binary value, ").append(t.getBinary(key).length).append(" bytes, value: ")
                            .append(new String(Hex.encodeHex(t.getBinary(key))));
                } else if (DATE_TOKENS.contains(key)) {
                    string.append(bytesToDate(t.getBinary(key)));
                } else {
                    string.append("unknown type, ").append(t.getBinary(key).length).append(" bytes");
                }
                string.append("\n");
            }
            return string.toString().substring(0, string.length() - 1);
        }
    }

    /** Class to stringify a Kerberos credential token. */
    private static class KerberosTokenStringifier implements Stringifier {

        /** An unmodifiable Set holding all names of tokens that are Strings. */
        private static final Set STRING_TOKENS;
        /** An unmodifiable Set holding all names of tokens that are arrays of binary data. */
        private static final Set BINARY_TOKENS;
        /** An unmodifiable Set holding all names of tokens that are arrays of binary data representing a timestamp. */
        private static final Set DATE_TOKENS;
        /** An unmodifiable Set holding all names of tokens that are arrays of binary data representing an integer. */
        private static final Set NUMBER_TOKENS;

        static {
            // initialise things for the toString() method
            String[] stringTokens = { "c", "s" };
            Set<String> strings = new TreeSet<String>();
            for (String s : stringTokens) {
                strings.add(s);
            }
            STRING_TOKENS = Collections.unmodifiableSet(strings);

            String[] binaryTokens = { "a", "k", "t" };
            Set<String> binary = new TreeSet<String>();
            for (String s : binaryTokens) {
                binary.add(s);
            }
            BINARY_TOKENS = Collections.unmodifiableSet(binary);

            String[] dateTokens = { "ta", "te", "tr", "ts" };
            Set<String> dates = new TreeSet<String>();
            for (String s : dateTokens) {
                dates.add(s);
            }
            DATE_TOKENS = Collections.unmodifiableSet(dates);

            String[] integerTokens = { "A", "D", "d", "f", "i", "K", "na", "nd" };
            Set<String> integers = new TreeSet<String>();
            for (String s : integerTokens) {
                integers.add(s);
            }
            NUMBER_TOKENS = Collections.unmodifiableSet(integers);
        }

        public String toString(Token t) {
            StringBuilder string = new StringBuilder();
            string.append("Kerberos token keys and values:\n");
            for (String key : t.kv.keySet()) {
                String searchKey = key.replaceFirst("[\\d]+$", "");
                string.append(key).append(": ");
                if (STRING_TOKENS.contains(searchKey)) {
                    string.append("'").append(t.getString(key)).append("'");
                } else if (BINARY_TOKENS.contains(searchKey)) {
                    string.append("binary value, ").append(t.getBinary(key).length).append(" bytes, value: ")
                            .append(new String(Hex.encodeHex(t.getBinary(key))));
                } else if (DATE_TOKENS.contains(searchKey)) {
                    string.append(bytesToDate(t.getBinary(key)));
                } else if (NUMBER_TOKENS.contains(searchKey)) {
                    string.append(bytesToInt(t.getBinary(key)));
                } else {
                    string.append("unknown type, ").append(t.getBinary(key).length).append(" bytes");
                }
                string.append("\n");
            }
            return string.toString().substring(0, string.length() - 1);
        }
    }
}