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

com.github.robtimus.obfuscation.json.ObfuscatingJsonGenerator 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
/*
 * ObfuscatingJsonGenerator.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.writer;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import javax.json.JsonArray;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonString;
import javax.json.JsonValue;
import javax.json.stream.JsonGenerator;
import javax.json.stream.JsonGeneratorFactory;
import com.github.robtimus.obfuscation.json.JSONObfuscator.PropertyConfigurer.ObfuscationMode;
import com.github.robtimus.obfuscation.support.LimitAppendable;

class ObfuscatingJsonGenerator implements JsonGenerator {

    private final JsonGeneratorFactory jsonGeneratorFactory;
    private final JsonGenerator originalDelegate;
    private final JSONObfuscatorWriter writer;
    private final LimitAppendable appendable;
    private final Map properties;
    private final boolean produceValidJSON;

    private final StringBuilder captured;
    private final Writer capturedWriter;

    private JsonGenerator delegate;

    private final Deque currentProperties = new ArrayDeque<>();

    @SuppressWarnings("resource")
    ObfuscatingJsonGenerator(JsonGeneratorFactory jsonGeneratorFactory, JSONObfuscatorWriter writer, LimitAppendable appendable,
            Map properties, boolean produceValidJSON) {

        this.jsonGeneratorFactory = jsonGeneratorFactory;
        this.originalDelegate = jsonGeneratorFactory.createGenerator(new DontCloseWriter(writer));
        this.writer = writer;
        this.appendable = appendable;
        this.properties = properties;
        this.produceValidJSON = produceValidJSON;

        this.captured = new StringBuilder();
        this.capturedWriter = writer(captured);

        this.delegate = originalDelegate;
    }

    // Writing will flush the jsonGenerator after each write, so writes are done to the correct writer at the time.
    // The try-finally will ensure that these flushes are not propagated to the writer's backing writer.

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator writeStartObject() {
        ObfuscatedProperty currentProperty = currentProperties.peekLast();
        if (currentProperty != null) {
            if (currentProperty.depth == 0) {
                // The start of the object that's being obfuscated
                ObfuscationMode obfuscationMode = currentProperty.config.forObjects;
                if (obfuscationMode == ObfuscationMode.OBFUSCATE) {
                    currentProperty.obfuscationMode = obfuscationMode;
                    currentProperty.depth++;

                    startObfuscating(currentProperty);
                } else if (obfuscationMode == ObfuscationMode.EXCLUDE) {
                    // There is an obfuscator for the object property, but the obfuscation mode prohibits obfuscating objects, so discard the property
                    currentProperties.removeLast();
                } else {
                    currentProperty.obfuscationMode = obfuscationMode;
                    currentProperty.depth++;
                }
            } else {
                // In a nested object or array that's being obfuscated; do nothing
                currentProperty.depth++;
            }
        }
        // else not obfuscating

        try {
            writer.preventFlush();
            delegate.writeStartObject();
            delegate.flush();
        } finally {
            writer.allowFlush();
        }
        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator writeStartObject(String name) {
        // Don't delegate but split instead
        writeKey(name);
        writeStartObject();
        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator writeKey(String name) {
        ObfuscatedProperty currentProperty = currentProperties.peekLast();
        if ((currentProperty == null || currentProperty.allowsOverriding()) && !appendable.limitExceeded()) {
            PropertyConfig config = properties.get(name);
            if (config != null) {
                currentProperty = new ObfuscatedProperty(config);
                currentProperties.addLast(currentProperty);
            }
        }
        // else in a nested object or array that's being obfuscated, or the destination limit has already been exceed; do nothing
        // The limitExceed check is added to prevent any complex logic being executed for content that will not be appended anyway

        try {
            writer.preventFlush();
            delegate.writeKey(name);
            delegate.flush();
        } finally {
            writer.allowFlush();
        }
        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator writeStartArray() {
        ObfuscatedProperty currentProperty = currentProperties.peekLast();
        if (currentProperty != null) {
            if (currentProperty.depth == 0) {
                // The start of the array that's being obfuscated
                ObfuscationMode obfuscationMode = currentProperty.config.forArrays;
                if (obfuscationMode == ObfuscationMode.OBFUSCATE) {
                    currentProperty.obfuscationMode = obfuscationMode;
                    currentProperty.depth++;

                    startObfuscating(currentProperty);
                } else if (obfuscationMode == ObfuscationMode.EXCLUDE) {
                    // There is an obfuscator for the array property, but the obfuscation mode prohibits obfuscating arrays, so discard the property
                    currentProperties.removeLast();
                } else {
                    currentProperty.obfuscationMode = obfuscationMode;
                    currentProperty.depth++;
                }
            } else {
                // In a nested object or array that's being obfuscated; do nothing
                currentProperty.depth++;
            }
        }
        // else not obfuscating

        try {
            writer.preventFlush();
            delegate.writeStartArray();
            delegate.flush();
        } finally {
            writer.allowFlush();
        }
        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator writeStartArray(String name) {
        // Don't delegate but split instead
        writeKey(name);
        writeStartArray();
        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(String name, JsonValue value) {
        // Don't delegate but split instead
        writeKey(name);
        write(value);
        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(String name, String value) {
        // Don't delegate but split instead
        writeKey(name);
        write(value);
        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(String name, BigInteger value) {
        // Don't delegate but split instead
        writeKey(name);
        write(value);
        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(String name, BigDecimal value) {
        // Don't delegate but split instead
        writeKey(name);
        write(value);
        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(String name, int value) {
        // Don't delegate but split instead
        writeKey(name);
        write(value);
        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(String name, long value) {
        // Don't delegate but split instead
        writeKey(name);
        write(value);
        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(String name, double value) {
        // Don't delegate but split instead
        writeKey(name);
        write(value);
        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(String name, boolean value) {
        // Don't delegate but split instead
        writeKey(name);
        write(value);
        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator writeNull(String name) {
        // Don't delegate but split instead
        writeKey(name);
        writeNull();
        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator writeEnd() {
        try {
            writer.preventFlush();
            delegate.writeEnd();
            delegate.flush();
        } finally {
            writer.allowFlush();
        }

        ObfuscatedProperty currentProperty = currentProperties.peekLast();
        if (currentProperty != null) {
            currentProperty.depth--;
            if (currentProperty.depth == 0) {
                // The end of obfuscating an object or array
                endObfuscating(currentProperty);

                currentProperties.removeLast();
            }
            // else still in a nested object that's being obfuscated
        }
        // else currently no object is being obfuscated
        return this;
    }

    private void startObfuscating(ObfuscatedProperty currentProperty) {
        if (currentProperty.obfuscateStructure()) {
            if (produceValidJSON) {
                // A new delegate is needed to be able to start a new object or array
                delegate = jsonGeneratorFactory.createGenerator(capturedWriter);
            } else {
                writer.startObfuscate(currentProperty.config.obfuscator);
            }
        }
    }

    private void endObfuscating(ObfuscatedProperty currentProperty) {
        if (currentProperty.obfuscateStructure()) {
            if (produceValidJSON) {
                // The delegate is a new generator around the writer around captured; close it and write the captured content as a string
                assert delegate != originalDelegate : "delegate must be a capturing generator"; //$NON-NLS-1$
                delegate.close();
                delegate = originalDelegate;
                writeQuoted(currentProperty, captured);
                captured.delete(0, captured.length());
            } else {
                // Obfuscation has started on writer; end obfuscation of the current object or array
                try {
                    writer.endObfuscate();
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            }
        }
    }

    @Override
    public JsonGenerator write(JsonValue value) {
        // Don't delegate but call correct write methods based on the type
        switch (value.getValueType()) {
            case OBJECT:
                write(value.asJsonObject());
                break;
            case ARRAY:
                write(value.asJsonArray());
                break;
            case STRING:
                write((JsonString) value);
                break;
            case NUMBER:
                write((JsonNumber) value);
                break;
            case TRUE:
                write(true);
                break;
            case FALSE:
                write(false);
                break;
            case NULL:
                writeNull();
                break;
            default:
                // Should not occur
                break;
        }

        return this;
    }

    @SuppressWarnings("resource")
    private void write(JsonObject object) {
        writeStartObject();
        for (Map.Entry entry : object.entrySet()) {
            write(entry.getKey(), entry.getValue());
        }
        writeEnd();
    }

    @SuppressWarnings("resource")
    private void write(JsonArray array) {
        writeStartArray();
        for (JsonValue element : array) {
            write(element);
        }
        writeEnd();
    }

    @SuppressWarnings("resource")
    private void write(JsonString string) {
        write(string.getString());
    }

    @SuppressWarnings("resource")
    private void write(JsonNumber number) {
        if (number.isIntegral()) {
            try {
                write(number.longValueExact());
            } catch (@SuppressWarnings("unused") ArithmeticException e) {
                write(number.bigIntegerValue());
            }
        } else {
            write(number.bigDecimalValue());
        }
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(String value) {
        ObfuscatedProperty currentProperty = currentProperties.peekLast();
        if (currentProperty != null && currentProperty.obfuscateScalar()) {
            writeString(currentProperty, value);

            if (currentProperty.depth == 0) {
                currentProperties.removeLast();
            }
        } else {
            // Not obfuscating, or in a nested object or array that's being obfuscated; just delegate
            delegate.write(value);
        }

        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(BigDecimal value) {
        ObfuscatedProperty currentProperty = currentProperties.peekLast();
        if (currentProperty != null && currentProperty.obfuscateScalar()) {
            writeNonString(currentProperty, value.toString());

            if (currentProperty.depth == 0) {
                currentProperties.removeLast();
            }
        } else {
            // Not obfuscating, or in a nested object or array that's being obfuscated; just delegate
            delegate.write(value);
        }

        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(BigInteger value) {
        ObfuscatedProperty currentProperty = currentProperties.peekLast();
        if (currentProperty != null && currentProperty.obfuscateScalar()) {
            writeNonString(currentProperty, value.toString());

            if (currentProperty.depth == 0) {
                currentProperties.removeLast();
            }
        } else {
            // Not obfuscating, or in a nested object or array that's being obfuscated; just delegate
            delegate.write(value);
        }

        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(int value) {
        ObfuscatedProperty currentProperty = currentProperties.peekLast();
        if (currentProperty != null && currentProperty.obfuscateScalar()) {
            writeNonString(currentProperty, Integer.toString(value));

            if (currentProperty.depth == 0) {
                currentProperties.removeLast();
            }
        } else {
            // Not obfuscating, or in a nested object or array that's being obfuscated; just delegate
            delegate.write(value);
        }

        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(long value) {
        ObfuscatedProperty currentProperty = currentProperties.peekLast();
        if (currentProperty != null && currentProperty.obfuscateScalar()) {
            writeNonString(currentProperty, Long.toString(value));

            if (currentProperty.depth == 0) {
                currentProperties.removeLast();
            }
        } else {
            // Not obfuscating, or in a nested object or array that's being obfuscated; just delegate
            delegate.write(value);
        }

        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(double value) {
        ObfuscatedProperty currentProperty = currentProperties.peekLast();
        if (currentProperty != null && currentProperty.obfuscateScalar()) {
            writeNonString(currentProperty, Double.toString(value));

            if (currentProperty.depth == 0) {
                currentProperties.removeLast();
            }
        } else {
            // Not obfuscating, or in a nested object or array that's being obfuscated; just delegate
            delegate.write(value);
        }

        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator write(boolean value) {
        ObfuscatedProperty currentProperty = currentProperties.peekLast();
        if (currentProperty != null && currentProperty.obfuscateScalar()) {
            writeNonString(currentProperty, Boolean.toString(value));

            if (currentProperty.depth == 0) {
                currentProperties.removeLast();
            }
        } else {
            // Not obfuscating, or in a nested object or array that's being obfuscated; just delegate
            delegate.write(value);
        }

        return this;
    }

    @Override
    @SuppressWarnings("resource")
    public JsonGenerator writeNull() {
        ObfuscatedProperty currentProperty = currentProperties.peekLast();
        if (currentProperty != null && currentProperty.obfuscateScalar()) {
            writeNonString(currentProperty, "null"); //$NON-NLS-1$

            if (currentProperty.depth == 0) {
                currentProperties.removeLast();
            }
        } else {
            // Not obfuscating, or in a nested object or array that's being obfuscated; just delegate
            delegate.writeNull();
        }

        return this;
    }

    private void writeString(ObfuscatedProperty currentProperty, CharSequence value) {
        writeQuoted(currentProperty, value);
    }

    private void writeNonString(ObfuscatedProperty currentProperty, CharSequence value) {
        if (produceValidJSON) {
            writeQuoted(currentProperty, value);
        } else {
            writeUnquoted(currentProperty, value);
        }
    }

    @SuppressWarnings("resource")
    private void writeQuoted(ObfuscatedProperty currentProperty, CharSequence value) {
        try {
            writer.preventFlush();
            delegate.write(currentProperty.config.obfuscator.obfuscateText(value).toString());
            delegate.flush();
        } finally {
            writer.allowFlush();
        }
    }

    @SuppressWarnings("resource")
    private void writeUnquoted(ObfuscatedProperty currentProperty, CharSequence value) {
        try {
            writer.preventFlush();
            writer.startUnquote();
            delegate.write(currentProperty.config.obfuscator.obfuscateText(value).toString());
            delegate.flush();
            writer.endUnquote();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        } finally {
            writer.allowFlush();
        }
    }

    @Override
    public void close() {
        delegate.close();
    }

    @Override
    public void flush() {
        delegate.flush();
    }

    private static final class ObfuscatedProperty {

        private final PropertyConfig config;
        private ObfuscationMode obfuscationMode;
        private int depth = 0;

        private ObfuscatedProperty(PropertyConfig config) {
            this.config = config;
        }

        private boolean allowsOverriding() {
            // OBFUSCATE and INHERITED do not allow overriding
            // No need to include EXCLUDE; if that occurs the ObfuscatedProperty is discarded
            return obfuscationMode == ObfuscationMode.INHERIT_OVERRIDABLE;
        }

        private boolean obfuscateStructure() {
            // Don't obfuscate the entire structure if Obfuscator.none() is used
            return config.performObfuscation && obfuscationMode == ObfuscationMode.OBFUSCATE;
        }

        private boolean obfuscateScalar() {
            // Don't obfuscate the scalar if Obfuscator.none() is used
            // Obfuscate if depth == 0 (the property is for the scalar itself),
            // or if the obfuscation mode is INHERITED or INHERITED_OVERRIDABLE (EXCLUDE is discarded)
            return config.performObfuscation
                    && (depth == 0 || obfuscationMode != ObfuscationMode.OBFUSCATE);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy