001    // Copyright 2009, 2012 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    //     http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry5.internal.services;
016    
017    import org.apache.tapestry5.SymbolConstants;
018    import org.apache.tapestry5.alerts.AlertManager;
019    import org.apache.tapestry5.internal.InternalConstants;
020    import org.apache.tapestry5.internal.TapestryInternalUtils;
021    import org.apache.tapestry5.internal.util.Base64InputStream;
022    import org.apache.tapestry5.internal.util.MacOutputStream;
023    import org.apache.tapestry5.ioc.annotations.Symbol;
024    import org.apache.tapestry5.services.ClientDataEncoder;
025    import org.apache.tapestry5.services.ClientDataSink;
026    import org.apache.tapestry5.services.URLEncoder;
027    import org.slf4j.Logger;
028    
029    import javax.crypto.spec.SecretKeySpec;
030    import java.io.BufferedInputStream;
031    import java.io.IOException;
032    import java.io.ObjectInputStream;
033    import java.io.UnsupportedEncodingException;
034    import java.security.Key;
035    import java.util.zip.GZIPInputStream;
036    
037    public class ClientDataEncoderImpl implements ClientDataEncoder
038    {
039        private final URLEncoder urlEncoder;
040    
041        private final Key hmacKey;
042    
043        public ClientDataEncoderImpl(URLEncoder urlEncoder, @Symbol(SymbolConstants.HMAC_PASSPHRASE) String passphrase,
044                                     Logger logger,
045                                     @Symbol(InternalConstants.TAPESTRY_APP_PACKAGE_PARAM)
046                                     String applicationPackageName, AlertManager alertManager) throws UnsupportedEncodingException
047        {
048            this.urlEncoder = urlEncoder;
049    
050            if (passphrase.equals(""))
051            {
052                String message = String.format("The symbol '%s' has not been configured. " +
053                        "This is used to configure hash-based message authentication of Tapestry data stored in forms, or in the URL. " +
054                        "You application is less secure, and more vulnerable to denial-of-service attacks, when this symbol is not configured.",
055                        SymbolConstants.HMAC_PASSPHRASE);
056    
057                // Now to really get the attention of the developer!
058    
059                alertManager.error(message);
060    
061                logger.error(message);
062    
063                // Override the blank parameter to set a default value. Use the application package name,
064                // which is justly slightly more secure than having a fixed default.
065                passphrase = applicationPackageName;
066            }
067    
068            hmacKey = new SecretKeySpec(passphrase.getBytes("UTF8"), "HmacSHA1");
069        }
070    
071        public ClientDataSink createSink()
072        {
073            try
074            {
075                return new ClientDataSinkImpl(urlEncoder, hmacKey);
076            } catch (IOException ex)
077            {
078                throw new RuntimeException(ex);
079            }
080        }
081    
082        public ObjectInputStream decodeClientData(String clientData)
083        {
084            // The clientData is Base64 that's been gzip'ed (i.e., this matches
085            // what ClientDataSinkImpl does).
086    
087            int colonx = clientData.indexOf(':');
088    
089            if (colonx < 0)
090            {
091                throw new IllegalArgumentException("Client data must be prefixed with its HMAC code.");
092            }
093    
094            // Extract the string presumably encoded by the server using the secret key.
095    
096            String storedHmacResult = clientData.substring(0, colonx);
097    
098            String clientStream = clientData.substring(colonx + 1);
099    
100            try
101            {
102                Base64InputStream b64in = new Base64InputStream(clientStream);
103    
104                validateHMAC(storedHmacResult, b64in);
105    
106                // After reading it once to validate, reset it for the actual read (which includes the GZip decompression).
107    
108                b64in.reset();
109    
110                BufferedInputStream buffered = new BufferedInputStream(new GZIPInputStream(b64in));
111    
112                return new ObjectInputStream(buffered);
113            } catch (IOException ex)
114            {
115                throw new RuntimeException(ex);
116            }
117        }
118    
119        private void validateHMAC(String storedHmacResult, Base64InputStream b64in) throws IOException
120        {
121            MacOutputStream macOs = MacOutputStream.streamFor(hmacKey);
122    
123            TapestryInternalUtils.copy(b64in, macOs);
124    
125            macOs.close();
126    
127            String actual = macOs.getResult();
128    
129            if (!storedHmacResult.equals(actual))
130            {
131                throw new IOException("Client data associated with the current request appears to have been tampered with " +
132                        "(the HMAC signature does not match).");
133            }
134        }
135    
136        public ObjectInputStream decodeEncodedClientData(String clientData) throws IOException
137        {
138            return decodeClientData(urlEncoder.decode(clientData));
139        }
140    }