    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}