com.cloudbees.plugins.credentials.SecretBytes.java Source code

Java tutorial

Introduction

Here is the source code for com.cloudbees.plugins.credentials.SecretBytes.java

Source

/*
 * The MIT License
 *
 * Copyright (c) 2016, CloudBees, Inc..
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
/*
Portions Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements; and to
You under the Apache License, Version 2.0.
 */
package com.cloudbees.plugins.credentials;

import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Util;
import hudson.util.Secret;
import java.io.Serializable;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.crypto.Cipher;
import jenkins.security.ConfidentialStore;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.IllegalClassException;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
 * An analogue of {@link Secret} to be used for efficient storage of {@link byte[]}. The serialized form will embed the
 * salt and padding so no two invocations of {@link #getEncryptedData()} will return the same result, but all will
 * decrypt to the same {@link #getPlainData()}. XStream serialization and Stapler form-binding will assume that
 * the {@link #toString()} representation is used (i.e. the Base64 encoded secret bytes wrapped with <code>{</code>
 * and <code>}</code>. If the string representation fails to decrypt (and is not wrapped
 *
 * @since 2.1.5
 */
public class SecretBytes implements Serializable {
    /**
     * The chunk size.
     */
    private static final int CHUNK_SIZE = 16;
    /**
     * The salt size.
     */
    private static final int SALT_SIZE = 8;
    /**
     * Standardize serialization.
     */
    private static final long serialVersionUID = 1L;
    /**
     * The key that encrypts the data on disk.
     */
    private static final CredentialsConfidentialKey KEY = new CredentialsConfidentialKey(SecretBytes.class, "KEY");
    /**
     * Our logger.
     */
    private static final Logger LOGGER = Logger.getLogger(SecretBytes.class.getName());
    /**
     * The unencrypted bytes.
     */
    @NonNull
    private final byte[] value;

    /**
     * Internal constructor.
     *
     * @param encrypted {@code} true if the supplied data is already encrypted, {@code false} if the supplied data is plain text.
     * @param value the data to wrap.
     * @see #fromBytes(byte[])
     * @see #fromString(String)
     */
    private SecretBytes(boolean encrypted, @NonNull byte[] value) {
        if (encrypted) {
            this.value = value.clone();
        } else {
            try {
                // copied from https://github
                // .com/codehaus-plexus/plexus-cipher/blob/6ab0e38df80beed9ab3227ffab938b21dcdf5505/src
                // /main/java/org/sonatype/plexus/components/cipher/PBECipher.java
                byte[] salt = ConfidentialStore.get().randomBytes(SALT_SIZE);
                Cipher cipher = KEY.encrypt(salt);
                byte[] encryptedBytes = cipher.doFinal(value);
                int len = encryptedBytes.length;
                byte padLen = (byte) (CHUNK_SIZE - (salt.length + len + 1) % CHUNK_SIZE);
                int totalLen = salt.length + len + padLen + 1;
                byte[] allEncryptedBytes = new byte[totalLen];
                byte[] padBytes = ConfidentialStore.get().randomBytes(padLen);
                System.arraycopy(salt, 0, allEncryptedBytes, 0, salt.length);
                allEncryptedBytes[salt.length] = padLen;
                System.arraycopy(encryptedBytes, 0, allEncryptedBytes, salt.length + 1, len);
                System.arraycopy(padBytes, 0, allEncryptedBytes, salt.length + 1 + len, padLen & 0xff);
                this.value = allEncryptedBytes;
            } catch (GeneralSecurityException e) {
                throw new Error(e); // impossible
            }
        }
    }

    /**
     * Returns the raw unencrypted data. The caller is responsible for zeroing out the returned {@link byte[]} after
     * use.
     *
     * @return the raw unencrypted data.
     */
    @NonNull
    public byte[] getPlainData() {
        try {
            int totalLen = value.length;
            byte[] salt = new byte[SALT_SIZE];
            System.arraycopy(value, 0, salt, 0, salt.length);
            byte padLen = value[salt.length];
            int len = totalLen - salt.length - 1 - (padLen & 0xff);
            byte[] encryptedBytes = new byte[len];
            System.arraycopy(value, salt.length + 1, encryptedBytes, 0, len);
            Cipher cipher = KEY.decrypt(salt);
            return cipher.doFinal(encryptedBytes);
        } catch (GeneralSecurityException e) {
            throw new Error(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        SecretBytes that = (SecretBytes) o;

        return Arrays.equals(value, that.value);

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return Arrays.hashCode(value);
    }

    /**
     * Returns the encrypted data.
     *
     * @return the encrypted data.
     */
    @NonNull
    public byte[] getEncryptedData() {
        return value.clone();
    }

    /**
     * Pattern matching a possible output of {@link #toString()}.
     * Basically, any Base64-encoded value.
     * You must then call {@link #decrypt} to eliminate false positives.
     */
    @Restricted(NoExternalUse.class)
    public static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("\\{[A-Za-z0-9+/]+={0,2}}");

    /**
     * Reverse operation of {@link #getEncryptedData()}. Returns null
     * if the given cipher text was invalid.
     *
     * @param data the bytes to decrypt.
     * @return the secret bytes or {@code null} if the data was not originally encrypted.
     */
    // copied from https://github.com/codehaus-plexus/plexus-cipher/blob/6ab0e38df80beed9ab3227ffab938b21dcdf5505/src
    // /main/java/org/sonatype/plexus/components/cipher/PBECipher.java
    @CheckForNull
    public static SecretBytes decrypt(byte[] data) {
        if (data == null || data.length <= SALT_SIZE + 1) {
            return null;
        }
        try {
            int totalLen = data.length;
            byte[] salt = new byte[SALT_SIZE];
            System.arraycopy(data, 0, salt, 0, salt.length);
            byte padLen = data[salt.length];
            int len = totalLen - salt.length - 1 - (padLen & 0xff);
            if (len < 0) {
                return null;
            }
            byte[] encryptedBytes = new byte[len];
            System.arraycopy(data, salt.length + 1, encryptedBytes, 0, len);
            Cipher cipher = KEY.decrypt(salt);
            cipher.doFinal(encryptedBytes);
            return new SecretBytes(true, data);
        } catch (GeneralSecurityException e) {
            return null;
        }
    }

    /**
     * Works just like {@link SecretBytes#getPlainData()} but avoids NPE when the secret is null.
     * To be consistent with {@link #fromBytes(byte[])}, this method doesn't distinguish
     * empty password and null password.
     *
     * @param s the secret bytes.
     * @return the decrypted bytes.
     */
    @NonNull
    public static byte[] getPlainData(@CheckForNull SecretBytes s) {
        return s == null ? new byte[0] : s.getPlainData();
    }

    /**
     * Attempts to treat the given bytes first as a cipher encrypted bytes, and if it doesn't work,
     * treat the given bytes as the unencrypted secret value.
     *
     * <p>
     * Useful for recovering a value from a form field.
     * If the supplied bytes are known to be unencrypted then the caller is responsible for zeroing out the supplied
     * {@link byte[]} afterwards.
     *
     * @param data the data to wrap or decrypt.
     * @return never null
     */
    public static SecretBytes fromBytes(byte[] data) {
        data = data == null ? new byte[0] : data;
        SecretBytes s = decrypt(data);
        if (s == null) {
            s = new SecretBytes(false, data);
        }
        return s;
    }

    /**
     * Attempts to treat the given bytes first as a cipher text, and if it doesn't work,
     * treat the given string as the unencrypted BASE-64 encoded byte array.
     *
     * <p>
     * Useful for recovering a value from a form field.
     *
     * Note: the caller is responsible for evicting the data from memory in the event that the data is
     * the unencrypted BASE-64 encoded plain data.
     *
     * @param data the string representation to decrypt.
     * @return never null
     */
    @NonNull
    public static SecretBytes fromString(String data) {
        data = Util.fixNull(data);
        SecretBytes s;
        try {
            int len = data.length();
            if (len >= 2 && ENCRYPTED_VALUE_PATTERN.matcher(data).matches()) {
                byte[] decoded = Base64.decodeBase64(data.substring(1, len - 1));
                s = decrypt(decoded);
                if (s != null) {
                    return s;
                }
            }
            s = new SecretBytes(false, Base64.decodeBase64(data));
        } catch (StringIndexOutOfBoundsException e) {
            // wasn't valid Base64
            s = new SecretBytes(false, new byte[0]);
        }
        return s;
    }

    /**
     * check if the given String is a SecretBytes text by attempting to decrypt it
     * @param data the string to check
     * @return true if the decryption was successful, false otherwise
     */
    public static boolean isSecretBytes(String data) {
        data = Util.fixNull(data);
        int len = data.length();
        if (len >= 2 && ENCRYPTED_VALUE_PATTERN.matcher(data).matches()) {
            byte[] decoded;
            try {
                decoded = Base64.decodeBase64(data.substring(1, len - 1));
            } catch (StringIndexOutOfBoundsException e) {
                // invalid Base64
                return false;
            }
            return decrypt(decoded) != null;
        }
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "{" + Base64.encodeBase64String(getEncryptedData()) + "}";
    }

    /**
     * Works just like {@link SecretBytes#toString()} but avoids NPE when the secret is null.
     * To be consistent with {@link #fromString(String)}, this method doesn't distinguish
     * empty password and null password.
     *
     * @param s the secret bytes.
     * @return the string representation.
     */
    public static String toString(SecretBytes s) {
        return s == null ? "" : s.toString();
    }

    /**
     * Our XStream converter.
     */
    public static final class ConverterImpl implements Converter {
        /**
         * {@inheritDoc}
         */
        public boolean canConvert(Class type) {
            return type == SecretBytes.class;
        }

        /**
         * {@inheritDoc}
         */
        public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
            SecretBytes src = (SecretBytes) source;
            writer.setValue(SecretBytes.toString(src));
        }

        /**
         * {@inheritDoc}
         */
        public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) {
            return fromString(reader.getValue());
        }
    }

    @Restricted(NoExternalUse.class)
    public static class StaplerConverterImpl implements org.apache.commons.beanutils.Converter {
        public SecretBytes convert(Class type, Object value) {
            if (value == null)
                return null;
            if (value instanceof String) {
                return SecretBytes.fromString((String) value);
            }
            throw new IllegalClassException(SecretBytes.class, value.getClass());
        }
    }
}