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 015 package org.apache.tapestry5.internal.services; 016 017 import org.apache.tapestry5.SymbolConstants; 018 import org.apache.tapestry5.alerts.AlertManager; 019 import org.apache.tapestry5.internal.InternalConstants; 020 import org.apache.tapestry5.internal.TapestryInternalUtils; 021 import org.apache.tapestry5.internal.util.Base64InputStream; 022 import org.apache.tapestry5.internal.util.MacOutputStream; 023 import org.apache.tapestry5.ioc.annotations.Symbol; 024 import org.apache.tapestry5.services.ClientDataEncoder; 025 import org.apache.tapestry5.services.ClientDataSink; 026 import org.apache.tapestry5.services.URLEncoder; 027 import org.slf4j.Logger; 028 029 import javax.crypto.spec.SecretKeySpec; 030 import java.io.BufferedInputStream; 031 import java.io.IOException; 032 import java.io.ObjectInputStream; 033 import java.io.UnsupportedEncodingException; 034 import java.security.Key; 035 import java.util.zip.GZIPInputStream; 036 037 public 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 }