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

com.swirlds.gui.model.Reference Maven / Gradle / Ivy

Go to download

Swirlds is a software platform designed to build fully-distributed applications that harness the power of the cloud without servers. Now you can develop applications with fairness in decision making, speed, trust and reliability, at a fraction of the cost of traditional server-based platforms.

There is a newer version: 0.45.1
Show newest version
/*
 * Copyright (C) 2023 Hedera Hashgraph, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.swirlds.gui.model;

import static com.swirlds.logging.LogMarker.EXCEPTION;

import java.math.BigInteger;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * Encapsulation of a Swirlds "reference", which is a 384-bit hash of a public key, file, swirld name, or
 * other entity. It supports converting between byte array and string, for several types of string.
 * Eventually, it may also allow converting between those and a QR code image. It is also legal to
 * instantiate with a 128-bit or 256-bit hash, but for some uses, 128 bits will not be secure against
 * birthday attacks, and 256 bits will not be secure against quantum computers. The US CNSA Suite requires
 * 384-bit hashes, and 256-bit AES keys.
 */
public class Reference {
    // data is the actual reference, either 16 or 32 bytes
    private byte[] data;
    private static final String digits = "0123456789" + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    /** use this for all logging, as controlled by the optional data/log4j2.xml file */
    private static final Logger logger = LogManager.getLogger(Reference.class);

    /**
     * Pass to the constructor 16, 32, or 48 bytes (128, 256, or 384 bits), which is the hash of the thing
     * being referenced (a key, a swirld name, a file, etc). A copy of data is made, so it is OK to change
     * the array after instantiating the Reference.
     *
     * The stored information will be one byte longer than the data, found by first appending a CRC8
     * checksum to the end, then XORing all the previous bytes with that checksum. Later, the checksum can
     * be checked by doing the XOR, then the checksum calculation, then comparing the result to the last
     * byte. When expressing a Reference in base 62 or as a list of words, the longer version is used.
     *
     * @param data
     * 		the 16 or 32 or 48 bytes constituting the reference
     */
    public Reference(byte[] data) {
        if (data == null) {
            logger.error(EXCEPTION.getMarker(), "data should not be null");
            throw new InvalidParameterException("data should not be null");
        }
        if (data == null || (data.length != 32 && data.length != 16 && data.length != 48)) {
            logger.error(EXCEPTION.getMarker(), "data.length() should be 16 or 32 or 48, not {}", data.length);
            throw new InvalidParameterException("data.length() should be 16 or 32 or 48, not " + data.length);
        }
        this.data = new byte[data.length + 1];
        // store in scrambled form so changing one bit of reference changes the entire string representation
        byte crc = crc8(data);
        this.data[data.length] = crc; // the crc goes at the end
        for (int i = 0; i < data.length; i++) {
            this.data[i] = (byte) (data[i] ^ crc); // the checksum also is XORed with all previous bytes
        }
    }

    /**
     * The natural log of 2, used in the log2() function
     */
    private static final double LOG_2 = Math.log(2);

    /**
     * Return the log base 2 of x.
     * The answer is exactly correct (without roundoff error) when x is an exact power of 2 that fits in a long.
     *
     * @param x
     * 		the number to take the log of
     * @return the log base 2 of x
     */
    private double log2(double x) {
        long n = (long) x;
        if (x == n && n > 0 && n == Long.highestOneBit(n)) { // if x is exactly a power of two
            return 63 - Long.numberOfLeadingZeros(n);
        }
        return Math.log(x) / LOG_2;
    }

    /**
     * Pass to the constructor a string representing the data. If the string starts and ends in <> and
     * contains only letters and digits in between, then it is considered a base62 encoding. Otherwise, it
     * is considered a list of words.
     *
     * @param dataString
     * 		the 16, 32, or 48 bytes constituting the reference
     */
    public Reference(String dataString) {
        if (dataString.matches("^<[a-zA-Z0-9]*>$")) { // base 62 encoding
            int lenString = dataString.length() - 2; // number of characters other than <>
            int len128Bits = (int) Math.ceil((128 + 8) / log2(digits.length()));
            int len256Bits = (int) Math.ceil((256 + 8) / log2(digits.length()));
            int len384Bits = (int) Math.ceil((384 + 8) / log2(digits.length()));
            if (lenString != len128Bits && lenString != len256Bits && lenString != len384Bits) {
                logger.error(
                        EXCEPTION.getMarker(),
                        " is not a proper encoding of 128, 256, or 384 bits "
                                + "because it has {} digits instead of {}, {}, or {}",
                        dataString,
                        lenString,
                        len128Bits,
                        len256Bits,
                        len384Bits);
                throw new InvalidParameterException(dataString
                        + " is not a proper encoding of 128, 256, or 384 bits because it has "
                        + lenString + " digits instead of " + len128Bits + ", "
                        + len256Bits + ", or " + len384Bits);
            }
            int arrayLen = (lenString == len128Bits) ? 16 + 1 : (lenString == len256Bits) ? 32 + 1 : 48 + 1;
            data = new byte[arrayLen];

            int[] inDigits = new int[lenString];
            for (int i = 0; i < lenString; i++) {
                inDigits[i] = digits.indexOf(dataString.charAt(i + 1));
            }
            int[] outDigits = convertRadix(inDigits, 62, 256, arrayLen);
            data = new byte[arrayLen];
            for (int i = 0; i < arrayLen; i++) {
                data[i] = (byte) outDigits[i];
            }
        } else { // list of words
            List words = WordList.words;
            int len128Bits = (int) Math.ceil((128 + 8) / log2(words.size()));
            int len256Bits = (int) Math.ceil((256 + 8) / log2(words.size()));
            int len384Bits = (int) Math.ceil((384 + 8) / log2(words.size()));

            List allWords = Collections.synchronizedList(new ArrayList());
            Matcher m = Pattern.compile("[a-zA-Z]+").matcher(dataString.toLowerCase());
            while (m.find()) {
                allWords.add(m.group());
            }
            int[] indices = new int[allWords.size()];
            int j = 0;
            for (String word : allWords) {
                indices[j++] = words.indexOf(word);
            }

            if (allWords.size() != len128Bits && allWords.size() != len256Bits) {
                throw new InvalidParameterException("there should be "
                        + len128Bits + ", " + len256Bits + ", or " + len384Bits
                        + " words, not " + allWords.size());
            }

            int dataLen = allWords.size() == len128Bits ? 16 + 1 : allWords.size() == len256Bits ? 32 + 1 : 48 + 1;
            int[] toDigits = convertRadix(indices, words.size(), 256, dataLen);
            data = new byte[dataLen];
            for (int i = 0; i < dataLen; i++) {
                data[i] = (byte) toDigits[i];
            }
        }
        byte[] dataUnscrambled = data.clone();
        byte crc = data[data.length - 1];
        for (int i = 0; i < data.length - 1; i++) {
            dataUnscrambled[i] ^= crc;
        }
        crc = crc8(dataUnscrambled);
        if (data[data.length - 1] != crc) {
            throw new InvalidParameterException("Invalid string: fails the cyclic redundency check");
        }
    }

    /**
     * Given a number in base fromRadix, with each digit being an integer in array number (number[0] is most
     * significant), return a new number in toRadix, where the array has toLength elements, left padded with
     * zeros if the number is too small, and chopping off the most significant digits if it is too large.
     *
     * @param number
     * 		the input number to convert
     * @param fromRadix
     * 		the base of input number
     * @param toRadix
     * 		the base of the returned number
     * @param toLength
     * 		the number of digits in the returned number
     * @return the number in the new base
     */
    private int[] convertRadix(int[] number, int fromRadix, int toRadix, int toLength) {
        int[] result = new int[toLength];
        BigInteger num = BigInteger.valueOf(0);
        BigInteger fromR = BigInteger.valueOf(fromRadix);
        BigInteger toR = BigInteger.valueOf(toRadix);
        for (int i = 0; i < number.length; i++) { // convert number ==> BigInteger
            num = num.multiply(fromR);
            num = num.add(BigInteger.valueOf(number[i]));
        }
        for (int i = toLength - 1; i >= 0; i--) { // convert BigInteger ==> result
            BigInteger[] pair = num.divideAndRemainder(toR);
            num = pair[0];
            result[i] = pair[1].intValue();
        }
        return result;
    }

    /**
     * calculate the checksum of all but the last byte in data. according to
     * http://www.ece.cmu.edu/~koopman/roses/dsn04/koopman04_crc_poly_embedded.pdf Koopman says 0xA6 (1
     * 0100110) is a good polynomial choice, which is x^8 + x^6 + x^3 + x^2 + x^1 . The following code uses
     * 0xB2 (1 0110010), which is 0xA6 with the bits reversed (after the first bit), which is needed for
     * this code.
     *
     * @param data
     * 		the data to find checksum for (where the last byte does not affect the checksum)
     * @return the checksum
     */
    public static byte crc8(byte[] data) {
        int crc = 0xFF;
        for (int i = 0; i < data.length - 1; i++) {
            crc ^= Byte.toUnsignedInt(data[i]);
            for (int j = 0; j < 8; j++) {
                crc = (crc >>> 1) ^ (((crc & 1) == 0) ? 0 : 0xB2);
            }
        }
        return (byte) (crc ^ 0xFF);
    }

    /**
     * Return the base-62 encoding, inside of <brackets>.
     */
    @Override
    public String toString() {
        return to62();
    }

    /**
     * Return this reference as an array of 16 or 32 or 48 bytes.
     *
     * @return the reference as an array of 16 or 32 or 48 bytes.
     */
    public byte[] toBytes() {
        byte[] result = Arrays.copyOfRange(data, 0, data.length - 1);
        byte crc = data[data.length - 1];
        for (int i = 0; i < data.length - 1; i++) { // return unscrambled data
            result[i] ^= crc;
        }
        return result;
    }

    /**
     * Return the first few characters of the string representing the reference, encoded in base 62
     *
     * @return
     */
    public String to62Prefix() {
        return to62().substring(0, 6) + "...>";
    }

    /**
     * Return a string representing the reference, encoded in base 62
     *
     * @return
     */
    public String to62() {
        int len = (int) Math.ceil(data.length * 8 / log2(digits.length()));
        int[] fromDigits = new int[data.length];
        int[] toDigits;
        char[] answer = new char[len];

        for (int i = 0; i < data.length; i++) {
            fromDigits[i] = Byte.toUnsignedInt(data[i]);
        }
        toDigits = convertRadix(fromDigits, 256, digits.length(), len);
        for (int i = 0; i < len; i++) {
            answer[i] = digits.charAt(toDigits[i]);
        }
        return "<" + (new String(answer)) + ">";
    }

    /**
     * Return a string representing the reference, made up of multiple lines of 4 words each, broken into
     * groups of 4 lines. The last group may have fewer lines, and its last line may have fewer words.
     *
     * @return the result as one string
     */
    public String toWords() {
        return toWords("+----------\n| ", " ", "\n| ", "\n| ", "\n|\n| ", "\n|\n| ", "\n+----------");

        // uncomment the following instead of the above, to indent every other group instead of skipping
        // a line every 4th line:
        //
        // return toWords("+----------\n| ", " ", "\n| ", "\n| ", "\n| ",
        // "\n| ", "\n+----------");
    }

    /**
     * Return a string representing the reference, made up of multiple lines of 4 words each, broken into
     * groups of 4 lines. The last group may have fewer lines, and its last line may have fewer words.
     *
     * @param indent
     * 		a string inserted at the start of each line
     * @return the result as one string
     */
    public String toWords(String indent) {
        return toWords(
                indent + "+----------\n" + indent + "| ",
                " ",
                "\n" + indent + "| ",
                "\n" + indent + "| ",
                "\n" + indent + "|\n" + indent + "| ",
                "\n" + indent + "|\n" + indent + "| ",
                "\n" + indent + "+----------");

        // uncomment the following instead of the above, to indent every other group instead of skipping
        // a line every 4th line:
        //
        // return toWords("+----------\n| ", " ", "\n| ", "\n| ", "\n| ",
        // "\n| ", "\n+----------");
    }

    /**
     * Return a string representing the reference, made up of multiple lines of 4 words each, broken into
     * groups of 3 lines. The last group may have fewer lines, and its last line may have fewer words. There
     * are no actual line breaks unless they are included in the parameters passed in.
     *
     * @param prefix
     * 		start of the entire string
     * @param wordSeparator
     * 		between adjacent words in a line of 4 words
     * @param lineSeparator1
     * 		between lines in a group of 3 lines, for every other group, starting with first
     * @param lineSeparator2
     * 		between lines in a group of 3 lines, for every other group, starting with second
     * @param groupSeparator1
     * 		between groups of 4 lines, for every other group, starting with first
     * @param groupSeparator2
     * 		between groups of 4 lines, for every other group, starting with second
     * @param suffix
     * 		end of the entire string
     * @return the result as one string
     */
    public String toWords(
            String prefix,
            String wordSeparator,
            String lineSeparator1,
            String lineSeparator2,
            String groupSeparator1,
            String groupSeparator2,
            String suffix) {
        List words = WordList.words;
        // need len words
        int len = (int) Math.ceil(data.length * 8 / log2(words.size()));
        String answer = "";

        int[] fromDigits = new int[data.length];
        for (int i = 0; i < data.length; i++) {
            fromDigits[i] = Byte.toUnsignedInt(data[i]);
        }
        int[] toDigits = convertRadix(fromDigits, 256, words.size(), len);

        for (int i = 0; i < len; i++) {
            if (i == 0) {
                answer += prefix;
            } else if (i % 24 == 0) {
                answer += groupSeparator1;
            } else if (i % 12 == 0) {
                answer += groupSeparator2;
            } else if (i % 4 == 0 && i % 24 < 12) {
                answer += lineSeparator1;
            } else if (i % 4 == 0) {
                answer += lineSeparator2;
            } else {
                answer += wordSeparator;
            }
            answer += words.get(toDigits[i]);
        }
        return new String(answer + suffix);
    }

    /**
     * return a string representation of the given byte array in hex, with a each byte becoming two
     * characters (including a leading zero, if necessary). Only use the array from index firstIndex to
     * index lastIndex. A lastIndex of -1 means the last element, -2 is the second to last, and so on. If
     * minIndex>maxIndex (after any wrapping of lastIndex), then return the null string.
     *
     * @param bytes
     * 		an array of bytes, some of which are to be converted
     * @param firstIndex
     * 		index of first element to convert
     * @param lastIndex
     * 		index of last element to convert (or -1 for last -2 for 2nd to last, etc)
     * @return a hex string of exactly two characters per converted byte
     */
    static String toHex(byte[] bytes, int firstIndex, int lastIndex) {
        String ans = "";
        int last = lastIndex >= 0 ? lastIndex : bytes.length + lastIndex;
        if (!(0 <= firstIndex && firstIndex <= last && last < bytes.length)) {
            return "";
        }
        for (int i = firstIndex; i <= last; i++) {
            ans += String.format("%02x", bytes[i] & 255);
        }
        return ans;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy