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

com.github.robtimus.obfuscation.json.JSONObfuscator Maven / Gradle / Ivy

Go to download

Provides functionality for obfuscating JSON using the Java API for Processing JSON (JSR 374)

There is a newer version: 2.0
Show newest version
/*
 * JSONObfuscator.java
 * Copyright 2020 Rob Spoor
 *
 * 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.github.robtimus.obfuscation.json;

import static com.github.robtimus.obfuscation.support.ObfuscatorUtils.appendAtMost;
import static com.github.robtimus.obfuscation.support.ObfuscatorUtils.checkStartAndEnd;
import static com.github.robtimus.obfuscation.support.ObfuscatorUtils.counting;
import static com.github.robtimus.obfuscation.support.ObfuscatorUtils.reader;
import static com.github.robtimus.obfuscation.support.ObfuscatorUtils.writer;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import javax.json.JsonException;
import javax.json.spi.JsonProvider;
import javax.json.stream.JsonGenerator;
import javax.json.stream.JsonGeneratorFactory;
import javax.json.stream.JsonParser;
import javax.json.stream.JsonParser.Event;
import javax.json.stream.JsonParsingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.robtimus.obfuscation.Obfuscator;
import com.github.robtimus.obfuscation.json.JSONObfuscator.PropertyConfigurer.ObfuscationMode;
import com.github.robtimus.obfuscation.support.CachingObfuscatingWriter;
import com.github.robtimus.obfuscation.support.CaseSensitivity;
import com.github.robtimus.obfuscation.support.CountingReader;
import com.github.robtimus.obfuscation.support.LimitAppendable;
import com.github.robtimus.obfuscation.support.MapBuilder;

/**
 * An obfuscator that obfuscates JSON properties in {@link CharSequence CharSequences} or the contents of {@link Reader Readers}.
 *
 * @author Rob Spoor
 */
public final class JSONObfuscator extends Obfuscator {

    private static final Logger LOGGER = LoggerFactory.getLogger(JSONObfuscator.class);

    private static final JsonProvider JSON_PROVIDER = JsonProvider.provider();

    private final Map properties;

    private final JsonGeneratorFactory jsonGeneratorFactory;

    private final boolean prettyPrint;
    private final boolean produceValidJSON;
    private final String malformedJSONWarning;

    private final long limit;
    private final String truncatedIndicator;

    private JSONObfuscator(ObfuscatorBuilder builder) {
        properties = builder.properties();

        prettyPrint = builder.prettyPrint;
        Map config = prettyPrint ? Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true) : Collections.emptyMap();
        jsonGeneratorFactory = JSON_PROVIDER.createGeneratorFactory(config);

        produceValidJSON = builder.produceValidJSON;
        malformedJSONWarning = builder.malformedJSONWarning;

        limit = builder.limit;
        truncatedIndicator = builder.truncatedIndicator;
    }

    @Override
    public CharSequence obfuscateText(CharSequence s, int start, int end) {
        checkStartAndEnd(s, start, end);
        StringBuilder sb = new StringBuilder(end - start);
        obfuscateText(s, start, end, sb);
        return sb.toString();
    }

    @Override
    public void obfuscateText(CharSequence s, int start, int end, Appendable destination) throws IOException {
        checkStartAndEnd(s, start, end);
        @SuppressWarnings("resource")
        Reader reader = reader(s, start, end);
        LimitAppendable appendable = appendAtMost(destination, limit);
        @SuppressWarnings("resource")
        JSONObfuscatorWriter writer = new JSONObfuscatorWriter(writer(appendable));
        obfuscateText(reader, writer, appendable);
        if (appendable.limitExceeded() && truncatedIndicator != null) {
            destination.append(String.format(truncatedIndicator, end - start));
        }
    }

    @Override
    public void obfuscateText(Reader input, Appendable destination) throws IOException {
        @SuppressWarnings("resource")
        CountingReader reader = counting(input);
        LimitAppendable appendable = appendAtMost(destination, limit);
        @SuppressWarnings("resource")
        JSONObfuscatorWriter writer = new JSONObfuscatorWriter(writer(appendable));
        obfuscateText(reader, writer, appendable);
        if (appendable.limitExceeded() && truncatedIndicator != null) {
            destination.append(String.format(truncatedIndicator, reader.count()));
        }
    }

    @SuppressWarnings("resource")
    private void obfuscateText(Reader input, JSONObfuscatorWriter writer, LimitAppendable appendable) throws IOException {
        try (JsonParser jsonParser = JSON_PROVIDER.createParser(new DontCloseReader(input));
                JsonGenerator jsonGenerator = createJsonGenerator(writer, appendable)) {

            // Cannot abort early as that could lead to errors due to incomplete JSON
            while (jsonParser.hasNext()) {
                Event event = jsonParser.next();
                switch (event) {
                    case START_OBJECT:
                        jsonGenerator.writeStartObject();
                        break;
                    case END_OBJECT:
                        jsonGenerator.writeEnd();
                        break;
                    case START_ARRAY:
                        jsonGenerator.writeStartArray();
                        break;
                    case END_ARRAY:
                        jsonGenerator.writeEnd();
                        break;
                    case KEY_NAME:
                        jsonGenerator.writeKey(jsonParser.getString());
                        break;
                    case VALUE_STRING:
                        jsonGenerator.write(jsonParser.getString());
                        break;
                    case VALUE_NUMBER:
                        jsonGenerator.write(jsonParser.getValue());
                        break;
                    case VALUE_TRUE:
                        jsonGenerator.write(true);
                        break;
                    case VALUE_FALSE:
                        jsonGenerator.write(false);
                        break;
                    case VALUE_NULL:
                        jsonGenerator.writeNull();
                        break;
                    default:
                        LOGGER.warn(Messages.JSONObfuscator.unexpectedEvent(event));
                        break;
                }
            }

            writer.assertNonObfuscating();
        } catch (JsonParsingException e) {
            LOGGER.warn(Messages.JSONObfuscator.malformedJSON.warning(), e);
            writer.endObfuscating();
            if (malformedJSONWarning != null) {
                writer.write(malformedJSONWarning);
            }
        } catch (JsonException e) {
            throw new IOException(e);
        }
    }

    JsonGenerator createJsonGenerator(JSONObfuscatorWriter writer, LimitAppendable appendable) {
        return new ObfuscatingJsonGenerator(jsonGeneratorFactory, writer, appendable, properties, produceValidJSON);
    }

    @Override
    public Writer streamTo(Appendable destination) {
        return new CachingObfuscatingWriter(this, destination);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || o.getClass() != getClass()) {
            return false;
        }
        JSONObfuscator other = (JSONObfuscator) o;
        return properties.equals(other.properties)
                && prettyPrint == other.prettyPrint
                && produceValidJSON == other.produceValidJSON
                && Objects.equals(malformedJSONWarning, other.malformedJSONWarning)
                && limit == other.limit
                && Objects.equals(truncatedIndicator, other.truncatedIndicator);
    }

    @Override
    public int hashCode() {
        return properties.hashCode() ^ Boolean.hashCode(prettyPrint) ^ Boolean.hashCode(produceValidJSON)
                ^ Objects.hashCode(malformedJSONWarning) ^ Long.hashCode(limit) ^ Objects.hashCode(truncatedIndicator);
    }

    @Override
    @SuppressWarnings("nls")
    public String toString() {
        return getClass().getName()
                + "[properties=" + properties
                + ",prettyPrint=" + prettyPrint
                + ",produceValidJSON=" + produceValidJSON
                + ",malformedJSONWarning=" + malformedJSONWarning
                + ",limit=" + limit
                + ",truncatedIndicator=" + truncatedIndicator
                + "]";
    }

    /**
     * Returns a builder that will create {@code JSONObfuscators}.
     *
     * @return A builder that will create {@code JSONObfuscators}.
     */
    public static Builder builder() {
        return new ObfuscatorBuilder();
    }

    /**
     * A builder for {@link JSONObfuscator JSONObfuscators}.
     *
     * @author Rob Spoor
     */
    public interface Builder {

        /**
         * Adds a property to obfuscate.
         * This method is an alias for {@link #withProperty(String, Obfuscator, CaseSensitivity)} with the last specified default case sensitivity
         * using {@link #caseSensitiveByDefault()} or {@link #caseInsensitiveByDefault()}. The default is {@link CaseSensitivity#CASE_SENSITIVE}.
         *
         * @param property The name of the property.
         * @param obfuscator The obfuscator to use for obfuscating the property.
         * @return An object that can be used to configure the property, or continue building {@link JSONObfuscator JSONObfuscators}.
         * @throws NullPointerException If the given property name or obfuscator is {@code null}.
         * @throws IllegalArgumentException If a property with the same name and the same case sensitivity was already added.
         */
        PropertyConfigurer withProperty(String property, Obfuscator obfuscator);

        /**
         * Adds a property to obfuscate.
         *
         * @param property The name of the property.
         * @param obfuscator The obfuscator to use for obfuscating the property.
         * @param caseSensitivity The case sensitivity for the property.
         * @return An object that can be used to configure the property, or continue building {@link JSONObfuscator JSONObfuscators}.
         * @throws NullPointerException If the given property name, obfuscator or case sensitivity is {@code null}.
         * @throws IllegalArgumentException If a property with the same name and the same case sensitivity was already added.
         */
        PropertyConfigurer withProperty(String property, Obfuscator obfuscator, CaseSensitivity caseSensitivity);

        /**
         * Sets the default case sensitivity for new properties to {@link CaseSensitivity#CASE_SENSITIVE}. This is the default setting.
         * 

* Note that this will not change the case sensitivity of any property that was already added. * * @return This object. */ Builder caseSensitiveByDefault(); /** * Sets the default case sensitivity for new properties to {@link CaseSensitivity#CASE_INSENSITIVE}. *

* Note that this will not change the case sensitivity of any property that was already added. * * @return This object. */ Builder caseInsensitiveByDefault(); /** * Indicates that by default properties will not be obfuscated if they are JSON objects or arrays. * This method is shorthand for calling both {@link #excludeObjectsByDefault()} and {@link #excludeArraysByDefault()}. *

* Note that this will not change what will be obfuscated for any property that was already added. * * @return This object. */ default Builder scalarsOnlyByDefault() { return excludeObjectsByDefault() .excludeArraysByDefault(); } /** * Indicates that by default properties will not be obfuscated if they are JSON objects. * This method is an alias for {@link #forObjectsByDefault(ObfuscationMode)} in combination with {@link ObfuscationMode#EXCLUDE}. *

* Note that this will not change what will be obfuscated for any property that was already added. * * @return This object. */ default Builder excludeObjectsByDefault() { return forObjectsByDefault(ObfuscationMode.EXCLUDE); } /** * Indicates that by default properties will not be obfuscated if they are JSON arrays. * This method is an alias for {@link #forArraysByDefault(ObfuscationMode)} in combination with {@link ObfuscationMode#EXCLUDE}. *

* Note that this will not change what will be obfuscated for any property that was already added. * * @return This object. */ default Builder excludeArraysByDefault() { return forArraysByDefault(ObfuscationMode.EXCLUDE); } /** * Indicates that by default properties will be obfuscated if they are JSON objects or arrays (default). * This method is shorthand for calling both {@link #includeObjectsByDefault()} and {@link #includeArraysByDefault()}. *

* Note that this will not change what will be obfuscated for any property that was already added. * * @return This object. */ default Builder allByDefault() { return includeObjectsByDefault() .includeArraysByDefault(); } /** * Indicates that by default properties will be obfuscated if they are JSON objects (default). * This method is an alias for {@link #forObjectsByDefault(ObfuscationMode)} in combination with {@link ObfuscationMode#OBFUSCATE}. *

* Note that this will not change what will be obfuscated for any property that was already added. * * @return This object. */ default Builder includeObjectsByDefault() { return forObjectsByDefault(ObfuscationMode.OBFUSCATE); } /** * Indicates that by default properties will be obfuscated if they are JSON arrays (default). * This method is an alias for {@link #forArraysByDefault(ObfuscationMode)} in combination with {@link ObfuscationMode#OBFUSCATE}. *

* Note that this will not change what will be obfuscated for any property that was already added. * * @return This object. */ default Builder includeArraysByDefault() { return forArraysByDefault(ObfuscationMode.OBFUSCATE); } /** * Indicates how to handle properties if they are JSON objects. The default is {@link ObfuscationMode#OBFUSCATE}. * This can be overridden per property using {@link PropertyConfigurer#forObjects(ObfuscationMode)} *

* Note that this will not change what will be obfuscated for any property that was already added. * * @param obfuscationMode The obfuscation mode that determines how to handle properties. * @return This object. * @throws NullPointerException If the given obfuscation mode is {@code null}. * @since 1.3 */ Builder forObjectsByDefault(ObfuscationMode obfuscationMode); /** * Indicates how to handle properties if they are JSON arrays. The default is {@link ObfuscationMode#OBFUSCATE}. * This can be overridden per property using {@link PropertyConfigurer#forArrays(ObfuscationMode)} *

* Note that this will not change what will be obfuscated for any property that was already added. * * @param obfuscationMode The obfuscation mode that determines how to handle properties. * @return This object. * @throws NullPointerException If the given obfuscation mode is {@code null}. * @since 1.3 */ Builder forArraysByDefault(ObfuscationMode obfuscationMode); /** * Sets whether or not to pretty-print obfuscated JSON. The default is {@code true}. * * @param prettyPrint {@code true} to pretty-print obfuscated JSON, or {@code false} otherwise. * @return This object. */ Builder withPrettyPrinting(boolean prettyPrint); /** * If called, obfuscation produces valid JSON, provided the input is valid JSON. This is done by converting obfuscated values to strings. * For values that were already strings this changes nothing. *

* An exception is made for {@link Obfuscator#none()}. This still allows skipping obfuscating values inside certain properties. *

* Note that if obfuscated objects or arrays are converted to strings, any line breaks that remain after obfuscation will be escaped. * * @return This object. */ Builder produceValidJSON(); /** * Sets the warning to include if a {@link JsonParsingException} is thrown. * This can be used to override the default message. Use {@code null} to omit the warning. * * @param warning The warning to include. * @return This object. */ Builder withMalformedJSONWarning(String warning); /** * Sets the limit for the obfuscated result. * * @param limit The limit to use. * @return An object that can be used to configure the handling when the obfuscated result exceeds a pre-defined limit, * or continue building {@link JSONObfuscator JSONObfuscators}. * @throws IllegalArgumentException If the given limit is negative. * @since 1.2 */ LimitConfigurer limitTo(long limit); /** * This method allows the application of a function to this builder. *

* Any exception thrown by the function will be propagated to the caller. * * @param The type of the result of the function. * @param f The function to apply. * @return The result of applying the function to this builder. */ default R transform(Function f) { return f.apply(this); } /** * Creates a new {@code JSONObfuscator} with the properties and obfuscators added to this builder. * * @return The created {@code JSONObfuscator}. */ JSONObfuscator build(); } /** * An object that can be used to configure a property that should be obfuscated. * * @author Rob Spoor */ public interface PropertyConfigurer extends Builder { /** * Indicates that properties with the current name will not be obfuscated if they are JSON objects or arrays. * This method is shorthand for calling both {@link #excludeObjects()} and {@link #excludeArrays()}. * * @return This object. */ default PropertyConfigurer scalarsOnly() { return excludeObjects() .excludeArrays(); } /** * Indicates that properties with the current name will not be obfuscated if they are JSON objects. * This method is an alias for {@link #forObjects(ObfuscationMode)} in combination with {@link ObfuscationMode#EXCLUDE}. * * @return This object. */ default PropertyConfigurer excludeObjects() { return forObjects(ObfuscationMode.EXCLUDE); } /** * Indicates that properties with the current name will not be obfuscated if they are JSON arrays. * This method is an alias for {@link #forArrays(ObfuscationMode)} in combination with {@link ObfuscationMode#EXCLUDE}. * * @return This object. */ default PropertyConfigurer excludeArrays() { return forArrays(ObfuscationMode.EXCLUDE); } /** * Indicates that properties with the current name will be obfuscated if they are JSON objects or arrays. * This method is shorthand for calling both {@link #includeObjects()} and {@link #includeArrays()}. * * @return This object. */ default PropertyConfigurer all() { return includeObjects() .includeArrays(); } /** * Indicates that properties with the current name will be obfuscated if they are JSON objects. * This method is an alias for {@link #forObjects(ObfuscationMode)} in combination with {@link ObfuscationMode#OBFUSCATE}. * * @return This object. */ default PropertyConfigurer includeObjects() { return forObjects(ObfuscationMode.OBFUSCATE); } /** * Indicates that properties with the current name will be obfuscated if they are JSON arrays. * This method is an alias for {@link #forArrays(ObfuscationMode)} in combination with {@link ObfuscationMode#OBFUSCATE}. * * @return This object. */ default PropertyConfigurer includeArrays() { return forArrays(ObfuscationMode.OBFUSCATE); } /** * Indicates how to handle properties if they are JSON objects. The default is {@link ObfuscationMode#OBFUSCATE}. * * @param obfuscationMode The obfuscation mode that determines how to handle properties. * @return This object. * @throws NullPointerException If the given obfuscation mode is {@code null}. * @since 1.3 */ PropertyConfigurer forObjects(ObfuscationMode obfuscationMode); /** * Indicates how to handle properties if they are JSON arrays. The default is {@link ObfuscationMode#OBFUSCATE}. * * @param obfuscationMode The obfuscation mode that determines how to handle properties. * @return This object. * @throws NullPointerException If the given obfuscation mode is {@code null}. * @since 1.3 */ PropertyConfigurer forArrays(ObfuscationMode obfuscationMode); /** * The possible ways to deal with nested objects and arrays. * * @author Rob Spoor * @since 1.3 */ enum ObfuscationMode { /** Don't obfuscate nested objects or arrays, but instead traverse into them. **/ EXCLUDE, /** Obfuscate nested objects and arrays completely. **/ OBFUSCATE, /** Don't obfuscate nested objects or arrays, but use the obfuscator for all nested scalar properties. **/ INHERIT, /** * Don't obfuscate nested objects or arrays, but use the obfuscator for all nested scalar properties. * If a nested property has its own obfuscator defined this will be used instead. **/ INHERIT_OVERRIDABLE, } } /** * An object that can be used to configure handling when the obfuscated result exceeds a pre-defined limit. * * @author Rob Spoor * @since 1.2 */ public interface LimitConfigurer extends Builder { /** * Sets the indicator to use when the obfuscated result is truncated due to the limit being exceeded. * There can be one place holder for the total number of characters. Defaults to {@code ... (total: %d)}. * Use {@code null} to omit the indicator. * * @param pattern The pattern to use as indicator. * @return This object. */ LimitConfigurer withTruncatedIndicator(String pattern); } private static final class ObfuscatorBuilder implements PropertyConfigurer, LimitConfigurer { private final MapBuilder properties; private boolean prettyPrint; private boolean produceValidJSON; private String malformedJSONWarning; private long limit; private String truncatedIndicator; // default settings private ObfuscationMode forObjectsByDefault; private ObfuscationMode forArraysByDefault; // per property settings private String property; private Obfuscator obfuscator; private CaseSensitivity caseSensitivity; private ObfuscationMode forObjects; private ObfuscationMode forArrays; private ObfuscatorBuilder() { properties = new MapBuilder<>(); prettyPrint = true; produceValidJSON = false; malformedJSONWarning = Messages.JSONObfuscator.malformedJSON.text(); limit = Long.MAX_VALUE; truncatedIndicator = "... (total: %d)"; //$NON-NLS-1$ forObjectsByDefault = ObfuscationMode.OBFUSCATE; forArraysByDefault = ObfuscationMode.OBFUSCATE; } @Override public PropertyConfigurer withProperty(String property, Obfuscator obfuscator) { addLastProperty(); properties.testEntry(property); this.property = property; this.obfuscator = obfuscator; this.caseSensitivity = null; this.forObjects = forObjectsByDefault; this.forArrays = forArraysByDefault; return this; } @Override public PropertyConfigurer withProperty(String property, Obfuscator obfuscator, CaseSensitivity caseSensitivity) { addLastProperty(); properties.testEntry(property, caseSensitivity); this.property = property; this.obfuscator = obfuscator; this.caseSensitivity = caseSensitivity; this.forObjects = forObjectsByDefault; this.forArrays = forArraysByDefault; return this; } @Override public Builder caseSensitiveByDefault() { properties.caseSensitiveByDefault(); return this; } @Override public Builder caseInsensitiveByDefault() { properties.caseInsensitiveByDefault(); return this; } @Override public Builder forObjectsByDefault(ObfuscationMode obfuscationMode) { forObjectsByDefault = Objects.requireNonNull(obfuscationMode); return this; } @Override public Builder forArraysByDefault(ObfuscationMode obfuscationMode) { forArraysByDefault = Objects.requireNonNull(obfuscationMode); return this; } @Override public PropertyConfigurer forObjects(ObfuscationMode obfuscationMode) { forObjects = Objects.requireNonNull(obfuscationMode); return this; } @Override public PropertyConfigurer forArrays(ObfuscationMode obfuscationMode) { forArrays = Objects.requireNonNull(obfuscationMode); return this; } @Override public Builder withPrettyPrinting(boolean prettyPrint) { this.prettyPrint = prettyPrint; return this; } @Override public Builder produceValidJSON() { produceValidJSON = true; return this; } @Override public Builder withMalformedJSONWarning(String warning) { malformedJSONWarning = warning; return this; } @Override public LimitConfigurer limitTo(long limit) { if (limit < 0) { throw new IllegalArgumentException(limit + " < 0"); //$NON-NLS-1$ } this.limit = limit; return this; } @Override public LimitConfigurer withTruncatedIndicator(String pattern) { this.truncatedIndicator = pattern; return this; } private Map properties() { return properties.build(); } private void addLastProperty() { if (property != null) { PropertyConfig propertyConfig = new PropertyConfig(obfuscator, forObjects, forArrays); if (caseSensitivity != null) { properties.withEntry(property, propertyConfig, caseSensitivity); } else { properties.withEntry(property, propertyConfig); } } property = null; obfuscator = null; caseSensitivity = null; forObjects = forObjectsByDefault; forArrays = forArraysByDefault; } @Override public JSONObfuscator build() { addLastProperty(); return new JSONObfuscator(this); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy