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