com.eucalyptus.auth.tokens.SecurityTokenManager.java Source code

Java tutorial

Introduction

Here is the source code for com.eucalyptus.auth.tokens.SecurityTokenManager.java

Source

/*************************************************************************
 * Copyright 2009-2013 Eucalyptus Systems, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses/.
 *
 * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
 * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
 * additional information or have any questions.
 ************************************************************************/
package com.eucalyptus.auth.tokens;

import static com.eucalyptus.auth.principal.TemporaryAccessKey.TemporaryKeyType;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.log4j.Logger;
import com.eucalyptus.auth.AccessKeys;
import com.eucalyptus.auth.Accounts;
import com.eucalyptus.auth.AuthException;
import com.eucalyptus.auth.principal.AccessKey;
import com.eucalyptus.auth.principal.Role;
import com.eucalyptus.auth.principal.TemporaryAccessKey;
import com.eucalyptus.auth.principal.User;
import com.eucalyptus.bootstrap.SystemIds;
import com.eucalyptus.crypto.Ciphers;
import com.eucalyptus.crypto.Crypto;
import com.eucalyptus.crypto.Digest;
import com.eucalyptus.crypto.util.B64;
import com.eucalyptus.util.Exceptions;
import com.google.common.base.Charsets;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;

/**
 * Security token manager for temporary credentials.
 */
public class SecurityTokenManager {

    private static final Logger log = Logger.getLogger(SecurityTokenManager.class);
    private static final Supplier<SecureRandom> randomSupplier = Crypto.getSecureRandomSupplier();
    private static final SecurityTokenManager instance = new SecurityTokenManager();

    /**
     * Issue a security token.
     *
     * <p>The token is tied to the provided access key and will be invalid if the
     * underlying access key is disabled or is removed.</p>
     *
     * <p>The credential associated with the token is of type
     * TemporaryAccessKey#Session.</p>
     *
     * @param user The user for the token
     * @param accessKey The originating access key for the token
     * @param durationSeconds The desired duration for the token
     * @return The newly issued security token
     * @throws AuthException If an error occurs
     * @see com.eucalyptus.auth.principal.TemporaryAccessKey.TemporaryKeyType#Session
     */
    @Nonnull
    public static SecurityToken issueSecurityToken(@Nonnull final User user, @Nullable final AccessKey accessKey,
            final int durationSeconds) throws AuthException {
        return instance.doIssueSecurityToken(user, accessKey, durationSeconds);
    }

    /**
     * Issue a security token.
     *
     * <p>The credential associated with the token is of type
     * TemporaryAccessKey#Access.</p>
     *
     * @param user The user for the token
     * @param durationSeconds The desired duration for the token
     * @return The newly issued security token
     * @throws AuthException If an error occurs
     * @see com.eucalyptus.auth.principal.TemporaryAccessKey.TemporaryKeyType#Access
     */
    @Nonnull
    public static SecurityToken issueSecurityToken(@Nonnull final User user, final int durationSeconds)
            throws AuthException {
        return instance.doIssueSecurityToken(user, durationSeconds);
    }

    /**
     * Issue a security token.
     *
     * <p>The credential associated with the token is of type
     * TemporaryAccessKey#Role.</p>
     *
     * @param role The role to to assume
     * @param durationSeconds The desired duration for the token
     * @return The newly issued security token
     * @throws AuthException If an error occurs
     * @see com.eucalyptus.auth.principal.TemporaryAccessKey.TemporaryKeyType#Role
     */
    @Nonnull
    public static SecurityToken issueSecurityToken(@Nonnull final Role role, final int durationSeconds)
            throws AuthException {
        return instance.doIssueSecurityToken(role, durationSeconds);
    }

    /**
     * Lookup the access key for a token.
     *
     * @param accessKeyId The identifier for the ephemeral access key
     * @param token The security token for the ephemeral access key
     * @return The access key
     * @throws AuthException If an error occurs
     */
    @Nonnull
    public static TemporaryAccessKey lookupAccessKey(@Nonnull final String accessKeyId, @Nonnull final String token)
            throws AuthException {
        return instance.doLookupAccessKey(accessKeyId, token);
    }

    /**
     *
     */
    @Nonnull
    protected SecurityToken doIssueSecurityToken(@Nonnull final User user, @Nullable final AccessKey accessKey,
            final int durationSeconds) throws AuthException {
        Preconditions.checkNotNull(user, "User is required");

        final AccessKey key = accessKey != null ? accessKey
                : Iterables.find(Objects.firstNonNull(user.getKeys(), Collections.<AccessKey>emptyList()),
                        AccessKeys.isActive(), null);

        if (key == null)
            throw new AuthException("Key not found for user");

        final long restrictedDurationMillis = restrictDuration(36, user.isAccountAdmin(), durationSeconds);

        if (!key.getUser().getUserId().equals(user.getUserId())) {
            throw new AuthException("Key not valid for user");
        }

        final EncryptedSecurityToken encryptedToken = new EncryptedSecurityToken(key.getAccessKey(),
                user.getUserId(), getCurrentTimeMillis(), restrictedDurationMillis);
        return new SecurityToken(encryptedToken.getAccessKeyId(), encryptedToken.getSecretKey(key.getSecretKey()),
                encryptedToken.encrypt(getEncryptionKey(encryptedToken.getAccessKeyId())),
                encryptedToken.getExpires());
    }

    /**
     *
     */
    @Nonnull
    protected SecurityToken doIssueSecurityToken(@Nonnull final User user, final int durationSeconds)
            throws AuthException {
        Preconditions.checkNotNull(user, "User is required");

        final String userToken = user.getToken();
        if (userToken == null || userToken.length() < 30) {
            throw new AuthException("Cannot generate token for user");
        }

        final long restrictedDurationMillis = restrictDuration(36, user.isAccountAdmin(), durationSeconds);

        final EncryptedSecurityToken encryptedToken = new EncryptedSecurityToken(null, user.getUserId(),
                getCurrentTimeMillis(), restrictedDurationMillis);
        return new SecurityToken(encryptedToken.getAccessKeyId(), encryptedToken.getSecretKey(userToken),
                encryptedToken.encrypt(getEncryptionKey(encryptedToken.getAccessKeyId())),
                encryptedToken.getExpires());
    }

    @Nonnull
    protected SecurityToken doIssueSecurityToken(@Nonnull final Role role, final int durationSeconds)
            throws AuthException {
        Preconditions.checkNotNull(role, "Role is required");

        final long restrictedDurationMillis = restrictDuration(1, false, durationSeconds);

        if (role.getSecret() == null || role.getSecret().length() < 30) {
            throw new AuthException("Cannot generate token for role");
        }

        final EncryptedSecurityToken encryptedToken = new EncryptedSecurityToken(role, getCurrentTimeMillis(),
                restrictedDurationMillis);
        return new SecurityToken(encryptedToken.getAccessKeyId(), encryptedToken.getSecretKey(role.getSecret()),
                encryptedToken.encrypt(getEncryptionKey(encryptedToken.getAccessKeyId())),
                encryptedToken.getExpires());
    }

    @Nonnull
    protected TemporaryAccessKey doLookupAccessKey(@Nonnull final String accessKeyId, @Nonnull final String token)
            throws AuthException {
        Preconditions.checkNotNull(accessKeyId, "Access key identifier is required");
        Preconditions.checkNotNull(token, "Token is required");

        final EncryptedSecurityToken encryptedToken;
        try {
            encryptedToken = EncryptedSecurityToken.decrypt(accessKeyId, getEncryptionKey(accessKeyId), token);
        } catch (GeneralSecurityException e) {
            log.debug(e, e);
            throw new AuthException("Invalid security token");
        }

        final String originatingAccessKeyId = encryptedToken.getOriginatingAccessKeyId();
        final String userId = encryptedToken.getUserId();
        final boolean active;
        final String secretKey;
        final User user;
        final TemporaryKeyType type;
        if (originatingAccessKeyId != null) {
            final AccessKey key = lookupAccessKeyById(originatingAccessKeyId);
            active = key.isActive();
            secretKey = encryptedToken.getSecretKey(key.getSecretKey());
            user = key.getUser();
            type = TemporaryKeyType.Session;
        } else if (userId != null) {
            user = lookupUserById(encryptedToken.getUserId());
            active = user.isEnabled();
            secretKey = encryptedToken.getSecretKey(Objects.firstNonNull(user.getToken(), ""));
            type = TemporaryKeyType.Access;
        } else {
            final Role role = lookupRoleById(encryptedToken.getRoleId());
            user = roleAsUser(role);
            active = true;
            secretKey = encryptedToken.getSecretKey(role.getSecret());
            type = TemporaryKeyType.Role;
        }

        return new TemporaryAccessKey() {
            private static final long serialVersionUID = 1L;

            @Override
            public Boolean isActive() {
                return active && encryptedToken.isValid();
            }

            @Override
            public String getAccessKey() {
                return encryptedToken.getAccessKeyId();
            }

            @Override
            public String getSecurityToken() {
                return token;
            }

            @Override
            public String getSecretKey() {
                return secretKey;
            }

            @Override
            public TemporaryKeyType getType() {
                return type;
            }

            @Override
            public Date getCreateDate() {
                return new Date(encryptedToken.getCreated());
            }

            @Override
            public Date getExpiryDate() {
                return new Date(encryptedToken.getExpires());
            }

            @Override
            public User getUser() throws AuthException {
                return user;
            }

            @Override
            public void setActive(final Boolean active) throws AuthException {
            }
        };
    }

    protected long getCurrentTimeMillis() {
        return System.currentTimeMillis();
    }

    protected AccessKey lookupAccessKeyById(final String accessKeyId) throws AuthException {
        return Accounts.lookupAccessKeyById(accessKeyId);
    }

    protected User lookupUserById(final String userId) throws AuthException {
        return Accounts.lookupUserById(userId);
    }

    protected Role lookupRoleById(final String roleId) throws AuthException {
        return Accounts.lookupRoleById(roleId);
    }

    protected User roleAsUser(final Role role) throws AuthException {
        return Accounts.roleAsUser(role);
    }

    protected String getSecurityTokenPassword() {
        return SystemIds.securityTokenPassword();
    }

    private long restrictDuration(final int maximumDurationHours, final boolean isAdmin, final int durationSeconds)
            throws SecurityTokenValidationException {
        long durationMillis = durationSeconds == 0 ? TimeUnit.HOURS.toMillis(12) : // use default
                TimeUnit.SECONDS.toMillis(durationSeconds);

        if (durationMillis > TimeUnit.HOURS.toMillis(maximumDurationHours)) {
            validationFailure(String.format("Invalid duration requested, maximum permitted duration is %s seconds.",
                    TimeUnit.HOURS.toSeconds(maximumDurationHours)));
        }

        if (durationMillis < TimeUnit.MINUTES.toMillis(15)) {
            validationFailure("Invalid duration requested, minimum permitted duration is 900 seconds.");
        }

        if (isAdmin && durationMillis > TimeUnit.HOURS.toMillis(1)) {
            durationMillis = TimeUnit.HOURS.toMillis(1);
        }

        return durationMillis;
    }

    private void validationFailure(final String message) throws SecurityTokenValidationException {
        throw new SecurityTokenValidationException(message);
    }

    private SecretKey getEncryptionKey(final String salt) {
        final MessageDigest digest = Digest.SHA256.get();
        digest.update(salt.getBytes(Charsets.UTF_8));
        digest.update(getSecurityTokenPassword().getBytes(Charsets.UTF_8));
        return new SecretKeySpec(digest.digest(), "AES");
    }

    private static final class EncryptedSecurityToken {
        private static final byte[] TOKEN_PREFIX = new byte[] { 'e', 'u', 'c', 'a', 0, 1 };

        private String accessKeyId;
        private String originatingId;
        private String nonce;
        private long created;
        private long expires;

        /**
         * Generate a new token
         */
        private EncryptedSecurityToken(final String originatingAccessKeyId, final String userId, final long created,
                final long durationMillis) {
            this(originatingAccessKeyId != null ? "$a$" + originatingAccessKeyId : "$u$" + userId, created,
                    durationMillis);
        }

        /**
         * Generate a new token
         */
        private EncryptedSecurityToken(final Role role, final long created, final long durationMillis) {
            this("$r$" + role.getRoleId(), created, durationMillis);
        }

        /**
        * Generate a new token
        */
        private EncryptedSecurityToken(final String originatingId, final long created, final long durationMillis) {
            this.accessKeyId = Crypto.generateAlphanumericId(20, "AKI");
            this.originatingId = originatingId;
            this.nonce = Crypto.generateSessionToken();
            this.created = created;
            this.expires = created + durationMillis;
        }

        /**
         * Reconstruct token
         */
        private EncryptedSecurityToken(final String accessKeyId, final String originatingId, final String nonce,
                final long created, final long expires) {
            this.accessKeyId = accessKeyId;
            this.originatingId = originatingId;
            this.nonce = nonce;
            this.created = created;
            this.expires = expires;
        }

        private String getAccessKeyId() {
            return accessKeyId;
        }

        public String getOriginatingAccessKeyId() {
            return getTrimmedIfPrefixed("$a$", originatingId);
        }

        public String getUserId() {
            return getTrimmedIfPrefixed("$u$", originatingId);
        }

        public String getRoleId() {
            return getTrimmedIfPrefixed("$r$", originatingId);
        }

        private String getTrimmedIfPrefixed(final String prefix, final String value) {
            return value.startsWith(prefix) ? value.substring(prefix.length()) : null;
        }

        public long getCreated() {
            return created;
        }

        private long getExpires() {
            return expires;
        }

        /**
         * Is the token within its validity period.
         */
        private boolean isValid() {
            final long now = System.currentTimeMillis();
            return now >= created && now < expires;
        }

        private String getSecretKey(final String secret) {
            final MessageDigest digest = Digest.SHA256.get();
            digest.update(secret.getBytes(Charsets.UTF_8));

            final StringBuilder keyBuilder = new StringBuilder(128);
            while (keyBuilder.length() < 40) {
                if (keyBuilder.length() > 0)
                    digest.update(keyBuilder.toString().getBytes(Charsets.UTF_8));
                digest.update(nonce.getBytes(Charsets.UTF_8));
                keyBuilder.append(B64.standard.encString(digest.digest()).replaceAll("\\p{Punct}", ""));
            }
            return keyBuilder.substring(0, 40);
        }

        private byte[] toBytes() {
            try {
                final SecurityTokenOutput out = new SecurityTokenOutput();
                out.writeInt(2); // format identifier
                out.writeString(originatingId);
                out.writeString(nonce);
                out.writeLong(created);
                out.writeLong(expires);
                return out.toByteArray();
            } catch (IOException e) {
                throw Exceptions.toUndeclared(e);
            }
        }

        private String encrypt(final SecretKey key) {
            try {
                final Cipher cipher = Ciphers.AES_GCM.get();
                final byte[] iv = new byte[32];
                randomSupplier.get().nextBytes(iv);
                cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
                final ByteArrayOutputStream out = new ByteArrayOutputStream();
                out.write(TOKEN_PREFIX);
                out.write(iv);
                out.write(cipher.doFinal(toBytes()));
                return B64.standard.encString(out.toByteArray());
            } catch (GeneralSecurityException | IOException e) {
                throw Exceptions.toUndeclared(e);
            }
        }

        private static EncryptedSecurityToken decrypt(final String accessKeyId, final SecretKey key,
                final String securityToken) throws GeneralSecurityException {
            try {
                final Cipher cipher = Ciphers.AES_GCM.get();
                final byte[] securityTokenBytes = B64.standard.dec(securityToken);
                if (securityTokenBytes.length < 64 + TOKEN_PREFIX.length
                        || !Arrays.equals(TOKEN_PREFIX, Arrays.copyOf(securityTokenBytes, TOKEN_PREFIX.length))) {
                    throw new GeneralSecurityException("Invalid token format");
                }

                cipher.init(Cipher.DECRYPT_MODE, key,
                        new IvParameterSpec(securityTokenBytes, TOKEN_PREFIX.length, 32));
                final int offset = TOKEN_PREFIX.length + 32;
                final SecurityTokenInput in = new SecurityTokenInput(
                        cipher.doFinal(securityTokenBytes, offset, securityTokenBytes.length - offset));
                if (in.readInt() != 2)
                    throw new GeneralSecurityException("Invalid token format");
                final String originatingAccessKeyIdOrUserId = in.readString();
                final String nonce = in.readString();
                final long created = in.readLong();
                final long expires = in.readLong();
                return new EncryptedSecurityToken(accessKeyId, originatingAccessKeyIdOrUserId, nonce, created,
                        expires);
            } catch (IOException e) {
                throw Exceptions.toUndeclared(e);
            }
        }
    }

    private static final class SecurityTokenOutput {
        private final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        private final Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
        private final DeflaterOutputStream out = new DeflaterOutputStream(byteStream, deflater);

        private void writeString(final String value) throws IOException {
            final byte[] data = value.getBytes(Charsets.UTF_8);
            writeInt(data.length);
            out.write(data);
        }

        private void writeInt(final int value) throws IOException {
            out.write(Ints.toByteArray(value));
        }

        private void writeLong(final long value) throws IOException {
            out.write(Longs.toByteArray(value));
        }

        private byte[] toByteArray() throws IOException {
            out.flush();
            out.close();
            return byteStream.toByteArray();
        }
    }

    private static final class SecurityTokenInput {
        private final InputStream in;

        private SecurityTokenInput(final byte[] data) {
            in = new InflaterInputStream(new ByteArrayInputStream(data));
        }

        private String readString() throws IOException {
            final byte[] data = new byte[readInt()];
            if (in.read(data) != data.length)
                throw new IOException();
            return new String(data, Charsets.UTF_8);
        }

        private int readInt() throws IOException {
            final byte[] data = new byte[4];
            if (in.read(data) != 4)
                throw new IOException();
            return Ints.fromByteArray(data);
        }

        private long readLong() throws IOException {
            final byte[] data = new byte[8];
            if (in.read(data) != 8)
                throw new IOException();
            return Longs.fromByteArray(data);
        }
    }
}