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.util.zip.GZIPInputStream;
036
037public 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}