001package squidpony;
002
003import squidpony.squidmath.CrossHash;
004import squidpony.squidmath.CrossHash.Mist;
005
006/**
007 * Tools for garbling Strings (making them appear to be gibberish) and degarbling earlier outputs to get the original
008 * inputs. This is like a weak form of encryption, and is probably enough to stop random users from editing saved files
009 * effectively. This allows a key as a String, long, or long array to be used to affect the garbling process.
010 * <br>
011 * A minor step of obfuscation could be to run some combination of garble calls with different keys and then require
012 * they be degarbled by degarble calls (with the same keys as before) in the reverse order of the garble calls.
013 * This is made more efficient with the {@link #garble(String, long[])} and {@link #degarble(String, long[])} methods,
014 * which avoid allocating multiple temporary char arrays when multiple keys are used. A more major step of obfuscation
015 * would be to run any garbling on already-compressed text, which {@link LZSPlus} does by using this class.
016 * <br>
017 * Created by Tommy Ettinger on 5/22/2017.
018 */
019public final class Garbler {
020
021    private Garbler()
022    {
023    }
024    
025    /**
026     * Get a long from this with {@code splitMix64(z += 0x9E3779B97F4A7C15L)}, where z is a long to use as state.
027     * 0x9E3779B97F4A7C15L can be changed for any odd long if the same number is used across calls.
028     * @param state long, must be changed with each call; {@code splitMix64(z += 0x9E3779B97F4A7C15L)} is recommended
029     * @return a pseudo-random long; all are possible
030     */
031    private static long splitMix64(long state) {
032        state = ((state >>> 30) ^ state) * 0xBF58476D1CE4E5B9L;
033        state = (state ^ (state >>> 27)) * 0x94D049BB133111EBL;
034        return state ^ (state >>> 31);
035    }
036
037    /**
038     * ThrustAltRNG.determine() in a stable form. Expects state to change by 1.
039     * @param state should change by 1
040     * @return a pseudo-random long; only about 2/3 of all 64-bit values are possible
041     */
042    private static long ta(long state) {
043        return (state = ((state *= 0x6C8E9CF570932BD5L) ^ (state >>> 25)) * (state | 0xA529L)) ^ (state >>> 23);
044    }
045
046    /**
047     * Mizuchi.nextLong() in a stable form. 
048     * Expects to be called with with {@code mizuchi(state = state * 0x369DEA0F31A53F85L + stream)}
049     * @param state should change with an LCG
050     * @return a pseudo-random long; all are possible
051     */
052    private static long mizuchi(long state)
053    {
054        return (state = (state ^ state >>> 23 ^ state >>> 47) * 0xAEF17502108EF2D9L) ^ state >>> 25;
055    }
056
057    /**
058     * Garbles text with the default key. This can be degarbled with {@link #degarble(String)}, which also uses the
059     * default key.
060     * @param text the text to garble
061     * @return a new String that appears unrelated to text and should look like gibberish
062     */
063    public static String garble(final String text) {
064        return garble(text, "HOWARD PHILLIPS LOVECRAFT, EVERYBODY!");
065    }
066
067    /**
068     * Garbles text with the given keyText. This can be degarbled with {@link #degarble(String, String)}, which must be
069     * given the same keyText.
070     * @param text the text to garble
071     * @param keyText used to determine the key this will use to garble text
072     * @return a new String that appears unrelated to text and should look like gibberish
073     */
074    public static String garble(final String text, final String keyText)
075    {
076        return garble(text, CrossHash.Water.hash64(keyText) ^ 0x7F4A7C15L);
077    }
078
079    /**
080     * Garbles text with the given key as a long. This can be degarbled with {@link #degarble(String, long)}, which must
081     * be given the same key.
082     * @param text the text to garble
083     * @param key the key this will use to garble text
084     * @return a new String that appears unrelated to text and should look like gibberish
085     */
086    public static String garble(final String text, final long key)
087    {
088        final char[] cs = text.toCharArray();
089        final int len = cs.length;
090        long state = ta(key);
091        final long increment = ta(state) | 1;
092        long wiggle;
093        for (int i = len - 1; i > 0; i--) {
094            wiggle = splitMix64(state += increment);
095            int r = (int) (((i+1L) * (wiggle & 0x7FFFFFFFL)) >>> 31);;
096            char c = cs[r];
097            cs[r] = cs[i];
098            cs[i] = (char) (c ^ wiggle >>> 59);
099        }
100        wiggle = splitMix64(state + increment);
101        cs[0] ^= wiggle >>> 59;
102        return String.valueOf(cs);
103    }
104
105    /**
106     * Given a garbled String that was produced by {@link #garble(String)} (using the default key), this reverses the
107     * garbling and gets the original String.
108     * @param garbled a String produced by a garble() method using the default key
109     * @return the original String before garbling, if the keys match
110     */
111    public static String degarble(final String garbled) {
112        return degarble(garbled, "HOWARD PHILLIPS LOVECRAFT, EVERYBODY!");
113    }
114    /**
115     * Given a garbled String that was produced by {@link #garble(String, String)} (using the given keyText), this
116     * reverses the garbling and gets the original String.
117     * @param garbled a String produced by a garble() method using the same keyText
118     * @param keyText the keyText that was used during garbling
119     * @return the original String before garbling, if the keys match
120     */
121    public static String degarble(final String garbled, final String keyText)
122    {
123        return degarble(garbled,CrossHash.Water.hash64(keyText) ^ 0x7F4A7C15L);
124    }
125
126    /**
127     * Given a garbled String that was produced by {@link #garble(String, long)} (using the given key), this reverses
128     * the garbling and gets the original String.
129     * @param garbled a String produced by a garble() method using the same keyText
130     * @param key the key that was used during garbling
131     * @return the original String before garbling, if the keys match
132     */
133    public static String degarble(final String garbled, final long key)
134    {
135        final char[] cs = garbled.toCharArray();
136        final int len = cs.length - 1;
137        long state = ta(key);
138        final long increment = ta(state) | 1;
139        long wiggle = splitMix64(state += increment * (len+1));
140        cs[0] ^= wiggle >>> 59;
141        for (int i = 1; i <= len; i++) {
142            wiggle = splitMix64(state -= increment);
143            int r = (int) (((i+1L) * (wiggle & 0x7FFFFFFFL)) >>> 31);
144            if(i == r)
145            {
146                cs[i] ^= wiggle >>> 59;
147            }
148            else {
149                char c = cs[r];
150                cs[r] = (char) (cs[i] ^ wiggle >>> 59);
151                cs[i] = c;
152            }
153        }
154        return String.valueOf(cs);
155    }
156
157    /**
158     * Garbles text with the given keys as a long array, effectively garbling the same text one time per item in keys.
159     * This can seen as a way to improve the quality of the shuffle by adding more bits of state to the key(s).
160     * The result can be degarbled with {@link #degarble(String, long[])}, which must be given the same keys. This
161     * method is more efficient than calling garble() repeatedly because it only allocates one temporary char array
162     * for the whole batch of keys, as opposed to needing one temporary array per key with repeated calls.
163     * @param text the text to garble
164     * @param keys the key array this will use to garble text, as a long array
165     * @return a new String that appears unrelated to text and should look like gibberish
166     */
167    public static String garble(final String text, final long[] keys)
168    {
169        if(keys == null)
170            return garble(text);
171        final char[] cs = text.toCharArray();
172        final int len = cs.length;
173        for (int k = 0; k < keys.length; k++) {
174            final long key = keys[k];
175            long state = ta(key);
176            final long increment = ta(state) | 1;
177            long wiggle;
178            for (int i = len - 1; i > 0; i--) {
179                wiggle = splitMix64(state += increment);
180                int r = (int) (((i+1L) * (wiggle & 0x7FFFFFFFL)) >>> 31);
181                char c = cs[r];
182                cs[r] = cs[i];
183                cs[i] = (char) (c ^ wiggle >>> 59);
184            }
185            wiggle = splitMix64(state + increment);
186            cs[0] ^= wiggle >>> 59;
187        }
188        return String.valueOf(cs);
189    }
190    /**
191     * Given a garbled String that was produced by {@link #garble(String, long[])} (using the given keys), this
192     * reverses the garbling and gets the original String. This is not the same as calling degarble() repeatedly, in
193     * part because this uses the keys in reverse order (just like every part of the degarbling process, it needs to be
194     * in reverse), and in part because this only creates one temporary char array for the whole batch of keys, instead
195     * of creating one new char array per repeated call.
196     * @param garbled a String produced by a garble() method using the same keyText
197     * @param keys the key array that was used during garbling
198     * @return the original String before garbling, if the keys match
199     */
200    public static String degarble(final String garbled, final long[] keys)
201    {
202        if(keys == null)
203            return degarble(garbled);
204        final char[] cs = garbled.toCharArray();
205        final int len = cs.length - 1;
206        for (int k = keys.length - 1; k >= 0; k--) {
207            final long key = keys[k];
208            long state = ta(key);
209            final long increment = ta(state) | 1;
210            long wiggle = splitMix64(state += increment * (len + 1));
211            cs[0] ^= wiggle >>> 59;
212            for (int i = 1; i <= len; i++) {
213                wiggle = splitMix64(state -= increment);
214                int r = (int) (((i+1L) * (wiggle & 0x7FFFFFFFL)) >>> 31);
215                if (i == r) {
216                    cs[i] ^= wiggle >>> 59;
217                } else {
218                    char c = cs[r];
219                    cs[r] = (char) (cs[i] ^ wiggle >>> 59);
220                    cs[i] = c;
221                }
222            }
223        }
224        return String.valueOf(cs);
225    }
226
227    /**
228     * If you need to produce an long array as a key for {@link #garble(String, long[])} when you only have a String,
229     * you can use this method if the String isn't too small (at least 8 char Strings should be fine). This produces a
230     * diverse array of longs without the correlation between items that you would get if you just generated a sequence
231     * of random longs from one small seed, by using multiple different {@link Mist} objects to hash the text.
232     * @param size the size of the key array to produce; larger key arrays take proportionately longer to process
233     * @param keyText the String to use as a basis for generating random-seeming numbers for keys
234     * @return a long array that can be given to {@link #garble(String, long[])} and {@link #degarble(String, long[])}
235     */
236    public static long[] makeKeyArray(final int size, final String keyText)
237    {
238        if(size <= 1) return new long[]{Mist.predefined[keyText.length() & 31].hash64(keyText)};
239        long[] keys = new long[size];
240        long ctr = keyText.length() * 181L + 0xB9A2842FL;
241        for (int i = 0; i < size; i++) {
242            ctr += (keys[i] = Mist.predefined[(int)splitMix64(ctr) & 31].hash64(keyText)) + 0xB9A2842FL;
243            keys[i] ^= ta(ctr);
244        }
245        return keys;
246    }
247}