    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}