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}