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

net.java.truelicense.obfuscate.ObfuscatedString Maven / Gradle / Ivy

Go to download

The TrueLicense @Obfuscate module provides core functionality for obfuscating constant string values in Java source and class files.

The newest version!
/*
 * Copyright (C) 2005-2015 Schlichtherle IT Services.
 * All rights reserved. Use is subject to license terms.
 */

package net.java.truelicense.obfuscate;

import javax.annotation.concurrent.ThreadSafe;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.Callable;

/**
 * Represents an obfuscated string.
 *
 * 

Motivation

*

* Software developers may wish to apply some licensing scheme to protect their * Intellectual Property (IP). * When implementing the licensing scheme, it is vital to obfuscate the Java * byte code. * Otherwise, a reverse engineer can simply replace the licensing code with a * stub in order to break the licensing scheme. *

* Likewise, all constant string expressions which are used to interact with * the licensing code need to get obfuscated, too. * Otherwise, the reverse engineer can simply search for known strings in the * Java byte code in order to identify the obfuscated licensing code or replace * its key strings with self-created key strings. * *

Usage

*

* This class specifically addresses the need to obfuscate constant string * expressions in Java byte code. * Note that general Java byte code obfuscation is out of scope - there are * other tools required to do this! *

* To obfuscate a constant string expression you need to pass it to the static * method {@link #obfuscate} at design time and replace it with the generated * Java source code expression. * At runtime, a duplicate of the original constant string expression will get * computed by the Java source code expression. * If you need to preserve the identity relation of constant string expressions, * then you need to add a call to {@link String#intern}()} to the duplicated * string. *

* A tool for Java byte code transformation (e.g. the TrueLicense Maven Plugin) * may automate some or all of these steps. * However, it needs to call {@link #array} instead of {@link #obfuscate} at * build time. * *

Security Considerations

*

* Note that obfuscation is not equal to encryption: * In contrast to the application of the simple and cheap obfuscation scheme in * this class, strong encryption would be comparably slow and expensive in * terms of resources - no matter what encryption algorithm would actually get * used. * Moreover, encrypting constant string expressions in Java byte code does not * effectively increase the security level when compared to simple obfuscation. * This is because an encryption key still needs to be made available to the * Java byte code and so could get eavesdropped by tracing JVM calls with the * standard JDK tool chain. * Thus, the simple obfuscation scheme implemented by this class is preferred * over complex encryption. * Conversely, if someone is proposing to encrypt constant string expressions * then please check if (s)he is trying to sell * snake oil. * * @see Obfuscate * @author Christian Schlichtherle */ // All constant string expressions in this class which are longer than two // characters need to get obfuscated as either an ObfuscatedString or, where // this would result in a stack overflow, as an array of Unicode characters. // This should help to prevent location of this class in obfuscated Java byte // code. @ThreadSafe public final class ObfuscatedString { /** The UTF-8 character set. */ private static final Charset charset = Charset.forName( new String(new char[] { '\u0055', '\u0054', '\u0046', '\u0038' })); // => "UTF8" /** * Returns a string containing an obfuscated Java source code expression * which computes a duplicate of the given string again. * This is equivalent to calling * {@link #java}(long[])}({@link #array}(String)}(s)). *

* As an example, calling this method with {@code "Hello world!"} as * its parameter may produce the result * {@code "new net.java.truelicense.obfuscate.runtime.ObfuscatedString(new long[] { * 0x39e61a665e397740l, 0xb681642064a96eael, 0xb8eb509886cc10f9l }).toString()"}. * If this Java source code is compiled and executed, it will * reproduce the original string {@code "Hello world!"} again. * * @param s the string to obfuscate. * This may not contain null characters. * @throws IllegalArgumentException If {@code s} contains a null character. * @return Some obfuscated Java source code to produce the given string * again. */ public static String obfuscate(final String s) { return java(array(s)); } /** * Returns a string containing an obfuscated Java source code expression * which computes a duplicate from the given array representation of the * original string. * * @param obfuscated the obfuscated array representation of the original * string. * @return a string containing an obfuscated Java source code expression * which computes a duplicate from the given array representation * of the original string. */ public static String java(final long[] obfuscated) { // Create and initialize a string builder to hold the generated Java // source code. final StringBuilder code = new StringBuilder(new ObfuscatedString(new long[] { 0x34ed718deb30a987l, 0xd52f133fc930a307l, 0xe0cf834b9f642a2fl, 0x457d8d023edf0e8l, 0x26f92c48a564815bl, 0xc783d4fc874ec9f5l, 0x25952072a49de44fl, 0x880316cb9f10d38fl, 0x18a131544f4ac2f5l, 0x3c8a17ccfcc80c32l }).toString()); // Append the seed as the first array element of the obfuscated string. // All values are represented in hexadecimal in order to keep the // Java source code as short as possible. appendHexLiteral(code, obfuscated[0]); for (int i = 1; i < obfuscated.length; i++) appendHexLiteral(code.append(", "), obfuscated[i]); return code .append(new ObfuscatedString(new long[] { 0xf1a8e07c6192640fl, 0xcb2e0f2d5721a1c9l, 0x459d2f4ea8c07d17l, 0x105d5eccb321565el }).toString()) .append(literal(new ObfuscatedString(obfuscated).toString()).replace("*/", "*\\/")) // escape */ in comment .append(new ObfuscatedString(new long[] { 0x41a2135076854cfdl, 0x68e9c0fa3cacd40el }).toString()) .toString(); } /** * Encodes the given string to a valid string literal in Java source code. * * @param s the string to quote and escape. * @return The transformed string. */ public static String literal(String s) { return '"' + s .replace("\\", new ObfuscatedString(new long[] { 0x780d9a8de89e5119l, 0x6f05f69a72c1e3b9l }).toString()) .replace("\"", new ObfuscatedString(new long[] { 0xb4315bc10c3fc55al, 0xb789e3d6c070e9bl }).toString()) + '"'; } /** * Returns an obfuscated array representation of the given string. * Obfuscation is performed by encoding the given string into UTF8 and then * XOR-ing a sequence of pseudo random numbers to it. * The result is decoded into an array of long integers as the obfuscated * array representation of the given string. * The sequence of pseudo random numbers is seeded with a random * number in order to provide a non-deterministic result for the obfuscated * array representation. * Hence, two subsequent calls with an equal string will produce equal * results by a chance of only 1/(248 - 1) - zero is not used as * a seed. * * @param s the string to obfuscate. * This may not contain null characters. * @throws IllegalArgumentException If {@code s} contains a null character. * @return an obfuscated array representation for the given string. */ public static long[] array(final String s) { if (-1 != s.indexOf(0)) { throw new IllegalArgumentException(new ObfuscatedString(new long[] { 0xf9e9dc47cf8a2b2cl, 0x37a43838b26a12f2l, 0x80a52ac841b35e9dl, 0x535f1d3b7f9b9eb5l, 0x10605244d9c009dal }).toString()); } // Encode the string to a byte array using UTF-8. final byte[] encoded = s.getBytes(charset); // Create the array to hold the result. final int el = encoded.length; final long[] obfuscated = new long[1 + (el + 7) / 8]; // Obtain a random long as the key, seed a new PRNG with it and save it. final long key = key(); final Random prng = new Random(key); obfuscated[0] = key; // Obfuscate the encoded string. // This will produce some null high order bytes in the last array // element iff the string was not encoded to a multiple of eight bytes. for (int i = 0, j = 0; i < el; i += 8) obfuscated[++j] = decode(encoded, i) ^ prng.nextLong(); return obfuscated; } private static long key() { final Random prng = new Random(); for (long key; ; ) if (0 != (key = prng.nextLong())) return key; } private static void appendHexLiteral(final StringBuilder sb, final long l) { // Obfuscation of the following character literals is futile because // they are too short to get found with a string search anyway. sb .append('\u0030').append('\u0078') .append(Long.toHexString(l)).append('\u006C'); // => "0x" + ... + "l" } /** * Decodes a long value from eight bytes in little endian order, * beginning at index {@code off}. * This is the inverse of {@link #encode(long, byte[], int)}. * If less than eight bytes are remaining in the array, * only these low order bytes are processed and the complementary high * order bytes of the returned value are set to zero. * * @param bytes the array containing the bytes to decode in little endian * order. * @param off the offset of the bytes in the array. * @return The decoded long value. */ private static long decode(final byte[] bytes, final int off) { final int end = Math.min(bytes.length, off + 8); long value = 0; for (int i = end; --i >= off; ) { value <<= 8; value |= bytes[i] & 0XFF; } return value; } /** * Encodes a long value to eight bytes in little endian order, * beginning at index {@code off}. * This is the inverse of {@link #decode}(byte[], int)}. * If less than eight bytes are remaining in the array, * only these low order bytes of the long value are processed and the * complementary high order bytes are ignored. * * @param value the long value to encode. * @param bytes the array which holds the encoded bytes upon return. * @param off the offset of the bytes in the array. */ private static void encode(long value, final byte[] bytes, final int off) { final int end = Math.min(bytes.length, off + 8); for (int i = off; i < end; i++) { bytes[i] = (byte) value; value >>= 8; } } private final long[] obfuscated; /** * Constructs an obfuscated string from the given array. * * @param obfuscated the array representation of the obfuscated string. * @throws ArrayIndexOutOfBoundsException If the provided array does not * contain at least one element. * @see #toString */ public ObfuscatedString(final long[] obfuscated) { this.obfuscated = obfuscated.clone(); this.obfuscated[0] = obfuscated[0]; // fail fast check } /** * Reproduces a copy of the original string from the obfuscated array * representation provided to the constructor. * * @return A copy of the original string in a new char array. */ public char[] toCharArray() { return new Codec() { @Override char[] decode(byte[] encoded, int length) throws Exception { return charset .newDecoder() .decode(ByteBuffer.wrap(encoded, 0, length)) .array(); } }.call(); } /** * Reproduces a copy of the original string from the obfuscated array * representation provided to the constructor. * * @return A copy of the original string. */ @Override public String toString() { return new Codec() { @Override String decode(byte[] encoded, int length) throws Exception { return new String(encoded, 0, length, charset); } }.call(); } /** * Equivalent to * {@link #toString}() toString()}.{@link String#intern}() intern()}. * * @return A canonical representation of the original string. */ public String toStringIntern() { return toString().intern(); } private abstract class Codec implements Callable { @Override public V call() { // Create the array to hold the encoded string. final long[] obfuscated = ObfuscatedString.this.obfuscated; final int ol = obfuscated.length; final byte[] encoded = new byte[8 * (ol - 1)]; // Load the key and seed a new PRNG with it. final long key = obfuscated[0]; final Random prng = new Random(key); // Encode the obfuscated string. // This will leave some null bytes at the end of the array iff the // original string was not encoded to a multiple of eight bytes. for (int i = 1; i < ol; i++) encode(obfuscated[i] ^ prng.nextLong(), encoded, 8 * (i - 1)); // Cut-off any null bytes and decode the string from the byte array // using UTF-8. int el = encoded.length; for (int j = el; 0 < j && 0 == encoded[--j]; el = j) { } try { return decode(encoded, el); } catch (final RuntimeException ex) { throw ex; } catch (final Exception ex) { throw new IllegalStateException(ex); } finally { Arrays.fill(encoded, 0, el, (byte) 0); } } abstract V decode(byte[] encoded, int length) throws Exception; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy