/************************************************************************* * Copyright 2009-2015 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.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.Map; import java.util.concurrent.ExecutionException; 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.InvalidAccessKeyAuthException; import com.eucalyptus.auth.euare.UserPrincipalImpl; import com.eucalyptus.auth.principal.AccessKey; import com.eucalyptus.auth.principal.BaseRole; import com.eucalyptus.auth.principal.SecurityTokenContent; import com.eucalyptus.auth.principal.SecurityTokenContentImpl; import com.eucalyptus.auth.principal.TemporaryAccessKey; import com.eucalyptus.auth.principal.User; import com.eucalyptus.auth.principal.UserPrincipal; import com.eucalyptus.auth.tokens.RoleSecurityTokenAttributes; import com.eucalyptus.auth.tokens.SecurityToken; import com.eucalyptus.auth.tokens.SecurityTokenManager; import com.eucalyptus.auth.tokens.SecurityTokenValidationException; import com.eucalyptus.auth.util.Identifiers; 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.eucalyptus.util.Pair; import com.google.common.base.Charsets; import com.google.common.base.MoreObjects; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; /** * Security token manager for temporary credentials. */ @SuppressWarnings( { "Guava", "StaticPseudoFunctionalStyleMethod", "OptionalUsedAsFieldOrParameterType" } ) public class SecurityTokenManagerImpl implements SecurityTokenManager.SecurityTokenProvider { private static final Logger log = Logger.getLogger( SecurityTokenManagerImpl.class ); private static final Supplier<SecureRandom> randomSupplier = Crypto.getSecureRandomSupplier(); private static final Supplier<String> securityTokenPasswordSupplier = Suppliers.memoize( SystemIds::securityTokenPassword ); private static final long creationSkewMillis = MoreObjects.firstNonNull( Longs.tryParse( System.getProperty( "com.eucalyptus.auth.tokens.creationSkewMillis", "5000" ) ), 5000L ); private static final int tokenCacheSize = MoreObjects.firstNonNull( Ints.tryParse( System.getProperty( "com.eucalyptus.auth.tokens.cache.maximumSize", "500" ) ), 500 ); private static final Cache<Pair<String,String>,SecurityTokenContent> tokenCache = CacheBuilder.newBuilder( ).expireAfterAccess( 5, TimeUnit.MINUTES ).maximumSize( tokenCacheSize ).build( ); /** * */ @Nonnull public SecurityToken doIssueSecurityToken( @Nonnull final User user, @Nullable final AccessKey accessKey, final int durationTruncationSeconds, final int durationSeconds ) throws AuthException { Preconditions.checkNotNull( user, "User is required" ); final AccessKey key = accessKey != null ? accessKey : Iterables.find( MoreObjects.firstNonNull( user.getKeys(), Collections.emptyList() ), AccessKeys.isActive(), null ); if ( key==null ) throw new AuthException("Key not found for user"); final long restrictedDurationMillis = restrictDuration( 36, durationTruncationSeconds, durationSeconds ); if ( !key.getPrincipal().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 public SecurityToken doIssueSecurityToken( @Nonnull final User user, final int durationTruncationSeconds, 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, durationTruncationSeconds, 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 public SecurityToken doIssueSecurityToken( @Nonnull final BaseRole role, @Nonnull final RoleSecurityTokenAttributes attributes, final int durationSeconds ) throws AuthException { Preconditions.checkNotNull( role, "Role is required" ); final long restrictedDurationMillis = restrictDuration( 1, 0, 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, attributes.asMap( ) ); return new SecurityToken( encryptedToken.getAccessKeyId(), encryptedToken.getSecretKey( role.getSecret() ), encryptedToken.encrypt( getEncryptionKey( encryptedToken.getAccessKeyId() ) ), encryptedToken.getExpires() ); } @Nonnull public 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 SecurityTokenContent securityTokenContent; try { final Pair<String,String> tokenKey = Pair.pair( accessKeyId, token ); securityTokenContent = tokenCache.get( tokenKey, () -> doDispatchingDecode( accessKeyId, token ) ); } catch ( ExecutionException e ) { log.debug( e, e ); throw new InvalidAccessKeyAuthException("Invalid security token"); } final String originatingAccessKeyId = securityTokenContent.getOriginatingAccessKeyId( ).orNull( ); final String userId = securityTokenContent.getOriginatingUserId().orNull( ); final UserPrincipal user; final TemporaryKeyType type; if ( originatingAccessKeyId != null ) { user = lookupByAccessKeyId( originatingAccessKeyId, securityTokenContent.getNonce() ); type = TemporaryKeyType.Session; } else if ( userId != null ) { user = lookupByUserById( userId, securityTokenContent.getNonce() ); type = TemporaryKeyType.Access; } else { final Optional<RoleSecurityTokenAttributes> roleAttributes = RoleSecurityTokenAttributes.forMap( securityTokenContent.getAttributes( ) ); user = lookupByRoleById( securityTokenContent.getOriginatingRoleId( ).get( ), roleAttributes.transform( RoleSecurityTokenAttributes::getSessionName ), securityTokenContent.getNonce( ) ); type = TemporaryKeyType.Role; } return new TemporaryAccessKey( ) { private static final long serialVersionUID = 1L; private UserPrincipal principal = new UserPrincipalImpl( user, Collections.<AccessKey>singleton( this ) ); @Override public Boolean isActive() { return user.isEnabled() && EncryptedSecurityToken.isValid( securityTokenContent ); } @Override public String getAccessKey() { return accessKeyId; } @Override public String getSecurityToken() { return token; } @Override public String getSecretKey() { return Iterables.getOnlyElement( user.getKeys( ) ).getSecretKey( ); } @Override public TemporaryKeyType getType() { return type; } @Override public Map<String, String> getAttributes() { return securityTokenContent.getAttributes( ); } @Override public Date getCreateDate() { return new Date(securityTokenContent.getCreated()); } @Override public Date getExpiryDate() { return new Date(securityTokenContent.getExpires()); } @Override public UserPrincipal getPrincipal() throws AuthException { return principal; } }; } @Nonnull public String doGenerateSecret( @Nonnull final String nonce, @Nonnull final String secret ) { return EncryptedSecurityToken.getSecretKey( nonce, secret ); } protected SecurityTokenContent doDispatchingDecode( final String accessKeyId, final String token ) throws AuthException { return Accounts.decodeSecurityToken( accessKeyId, token ); } @Nonnull public SecurityTokenContent doDecode( final String accessKeyId, final String token ) throws AuthException { final EncryptedSecurityToken encryptedSecurityToken; try { encryptedSecurityToken = EncryptedSecurityToken.decrypt( accessKeyId, getEncryptionKey( accessKeyId ), token ); } catch ( GeneralSecurityException e ) { throw new AuthException( "Unable to decode token", e ); } return new SecurityTokenContentImpl( Optional.fromNullable( encryptedSecurityToken.getOriginatingAccessKeyId() ), Optional.fromNullable( encryptedSecurityToken.getUserId() ), Optional.fromNullable( encryptedSecurityToken.getRoleId() ), encryptedSecurityToken.getNonce( ), encryptedSecurityToken.getCreated( ), encryptedSecurityToken.getExpires( ), encryptedSecurityToken.getAttributes( ) ); } protected long getCurrentTimeMillis() { return System.currentTimeMillis(); } protected UserPrincipal lookupByUserById( final String userId, final String nonce ) throws AuthException { return Accounts.lookupCachedPrincipalByUserId( userId, nonce ); } protected UserPrincipal lookupByRoleById( final String roleId, final Optional<String> sessionName, final String nonce ) throws AuthException { return Accounts.lookupCachedPrincipalByRoleId( sessionName.transform( session -> roleId + ":" + session ).or( roleId ), nonce ); } protected UserPrincipal lookupByAccessKeyId( final String accessKeyId, final String nonce ) throws AuthException { return Accounts.lookupCachedPrincipalByAccessKeyId( accessKeyId, nonce ); } protected String getSecurityTokenPassword() { return securityTokenPasswordSupplier.get( ); } private long restrictDuration( final int maximumDurationHours, final int durationTruncationSeconds, 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 ( durationTruncationSeconds > 0 && durationMillis > TimeUnit.SECONDS.toMillis( durationTruncationSeconds ) ) { durationMillis = TimeUnit.SECONDS.toMillis( durationTruncationSeconds ); } 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" ); } /** * Immutable token representation * * Format v3 adds a map for arbitrary attributes. */ private static final class EncryptedSecurityToken { private static final byte[] TOKEN_PREFIX = new byte[]{ 'e', 'u', 'c', 'a', 0, 1 }; private final String accessKeyId; private final String originatingId; private final String nonce; private final long created; private final long expires; private final ImmutableMap<String,String> attributes; /** * 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, null ); } /** * Generate a new token */ private EncryptedSecurityToken( final BaseRole role, final long created, final long durationMillis, final Map<String,String> attributes ) { this( "$r$" + role.getRoleId( ), created, durationMillis, attributes ); } /** * Generate a new token */ private EncryptedSecurityToken( final String originatingId, final long created, final long durationMillis, final Map<String,String> attributes ) { this.accessKeyId = Identifiers.generateAccessKeyIdentifier( ); this.originatingId = originatingId; this.nonce = Crypto.generateSessionToken(); this.created = created; this.expires = created + durationMillis; this.attributes = attributes == null ? ImmutableMap.of( ) : ImmutableMap.copyOf( attributes ); } /** * Reconstruct token */ private EncryptedSecurityToken( final String accessKeyId, final String originatingId, final String nonce, final long created, final long expires, final Map<String,String> attributes ) { this.accessKeyId = accessKeyId; this.originatingId = originatingId; this.nonce = nonce; this.created = created; this.expires = expires; this.attributes = ImmutableMap.copyOf( attributes ); } private String getAccessKeyId() { return accessKeyId; } public String getOriginatingAccessKeyId() { return getTrimmedIfPrefixed( "$a$", originatingId ); } public String getNonce() { return nonce; } 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; } public Map<String, String> getAttributes( ) { return attributes; } /** * Is the token within its validity period. */ private static boolean isValid( final SecurityTokenContent token ) { final long now = System.currentTimeMillis(); return ( now + creationSkewMillis ) >= token.getCreated( ) && now < token.getExpires( ); } private String getSecretKey( final String secret ) { return getSecretKey( nonce, secret ); } static String getSecretKey( final String nonce, 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(3); // format identifier out.writeString(originatingId); out.writeString(nonce); out.writeLong(created); out.writeLong(expires); out.writeInt(attributes.size()); for ( final Map.Entry<String,String> entry : attributes.entrySet( ) ) { out.writeString( entry.getKey( ) ); out.writeString( entry.getValue( ) ); } 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 ), randomSupplier.get( ) ); 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 ), randomSupplier.get( ) ); final int offset = TOKEN_PREFIX.length + 32; final SecurityTokenInput in = new SecurityTokenInput( cipher.doFinal( securityTokenBytes, offset, securityTokenBytes.length-offset ) ); final int version = in.readInt(); if ( version != 2 && version != 3 ) 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(); final Map<String,String> attributes = Maps.newHashMap( ); if ( version >= 3 ) { final int entries = in.readInt( ); for ( int i=0; i<entries; i++ ) { attributes.put( in.readString( ), in.readString( ) ); } } return new EncryptedSecurityToken( accessKeyId, originatingAccessKeyIdOrUserId, nonce, created, expires, attributes ); } 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 ); } } }