    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    }