Skip to content

Commit

Permalink
TAP5-2008: Implement HMAC signatures on object streams stored on the …
Browse files Browse the repository at this point in the history
…client
  • Loading branch information
Howard M. Lewis Ship committed Oct 4, 2012
1 parent 9001a1e commit 95846b1
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,6 @@ public class SymbolConstants
* Prefix to be used for all resource paths, used to recognize which requests are for assets. This value
* is appended to the context path and the (optional {@linkplain #APPLICATION_FOLDER application folder}.
* Its default is "assets". It may contain slashes, but should not begin or end with one.
*
*/
public static final String ASSET_PATH_PREFIX = "tapestry.asset-path-prefix";

Expand All @@ -359,4 +358,15 @@ public class SymbolConstants
* @since 5.4
*/
public static final String CONTEXT_PATH = "tapestry.context-path";

/**
* A passphrase used as the basis of hash-based message authentication (HMAC) for any object stream data stored on
* the client. The default phrase is the empty string, which will result in a logged runtime <em>error</em>.
* You should configure this to a reasonable value (longer is better) and ensure that all servers in your cluster
* share the same value (configuring this in code, rather than the command line, is preferred).
*
* @see org.apache.tapestry5.services.ClientDataEncoder
* @since 5.3.6
*/
public static final String HMAC_PASSPHRASE = "tapestry.hmac-passphrase";
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2009 The Apache Software Foundation
// Copyright 2009, 2012 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -14,32 +14,54 @@

package org.apache.tapestry5.internal.services;

import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.internal.TapestryInternalUtils;
import org.apache.tapestry5.internal.util.Base64InputStream;
import org.apache.tapestry5.internal.util.MacOutputStream;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.services.ClientDataEncoder;
import org.apache.tapestry5.services.ClientDataSink;
import org.apache.tapestry5.services.URLEncoder;
import org.slf4j.Logger;

import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.UnsupportedEncodingException;
import java.security.Key;
import java.util.zip.GZIPInputStream;

public class ClientDataEncoderImpl implements ClientDataEncoder
{
private final URLEncoder urlEncoder;

public ClientDataEncoderImpl(URLEncoder urlEncoder)
private final Key hmacKey;

public ClientDataEncoderImpl(URLEncoder urlEncoder, @Symbol(SymbolConstants.HMAC_PASSPHRASE) String passphrase, Logger logger) throws UnsupportedEncodingException
{
this.urlEncoder = urlEncoder;

if (passphrase.equals(""))
{
logger.error(String.format("The symbol '%s' has not been configured. " +
"This is used to configure hash-based message authentication of Tapestry data stored in forms, or in the URL. " +
"You application is less secure, and more vulnerable to denial-of-service attacks, when this symbol is left unconfigured.",
SymbolConstants.HMAC_PASSPHRASE));

// Errors at lower levels if the passphrase is empty, so override the parameter to set a default value.
passphrase = "DEFAULT";
}

hmacKey = new SecretKeySpec(passphrase.getBytes("UTF8"), "HmacSHA1");
}

public ClientDataSink createSink()
{
try
{
return new ClientDataSinkImpl(urlEncoder);
}
catch (IOException ex)
return new ClientDataSinkImpl(urlEncoder, hmacKey);
} catch (IOException ex)
{
throw new RuntimeException(ex);
}
Expand All @@ -48,21 +70,57 @@ public ClientDataSink createSink()
public ObjectInputStream decodeClientData(String clientData)
{
// The clientData is Base64 that's been gzip'ed (i.e., this matches
// what ClientDataSinkImpl does.
// what ClientDataSinkImpl does).

int colonx = clientData.indexOf(':');

if (colonx < 0)
{
throw new IllegalArgumentException("Client data must be prefixed with its HMAC code.");
}

// Extract the string presumably encoded by the server using the secret key.

String storedHmacResult = clientData.substring(0, colonx);

String clientStream = clientData.substring(colonx + 1);

try
{
BufferedInputStream buffered = new BufferedInputStream(
new GZIPInputStream(new Base64InputStream(clientData)));
Base64InputStream b64in = new Base64InputStream(clientStream);

validateHMAC(storedHmacResult, b64in);

// After reading it once to validate, reset it for the actual read (which includes the GZip decompression).

b64in.reset();

BufferedInputStream buffered = new BufferedInputStream(new GZIPInputStream(b64in));

return new ObjectInputStream(buffered);
}
catch (IOException ex)
} catch (IOException ex)
{
throw new RuntimeException(ex);
}
}

private void validateHMAC(String storedHmacResult, Base64InputStream b64in) throws IOException
{
MacOutputStream macOs = MacOutputStream.streamFor(hmacKey);

TapestryInternalUtils.copy(b64in, macOs);

macOs.close();

String actual = macOs.getResult();

if (!storedHmacResult.equals(actual))
{
throw new IOException("Client data associated with the current request appears to have been tampered with " +
"(the HMAC signature does not match).");
}
}

public ObjectInputStream decodeEncodedClientData(String clientData) throws IOException
{
return decodeClientData(urlEncoder.decode(clientData));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2009 The Apache Software Foundation
// Copyright 2009, 2012 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -15,13 +15,16 @@
package org.apache.tapestry5.internal.services;

import org.apache.tapestry5.internal.util.Base64OutputStream;
import org.apache.tapestry5.internal.util.MacOutputStream;
import org.apache.tapestry5.internal.util.TeeOutputStream;
import org.apache.tapestry5.services.ClientDataSink;
import org.apache.tapestry5.services.URLEncoder;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.security.Key;
import java.util.zip.GZIPOutputStream;

public class ClientDataSinkImpl implements ClientDataSink
Expand All @@ -34,12 +37,17 @@ public class ClientDataSinkImpl implements ClientDataSink

private boolean closed;

public ClientDataSinkImpl(URLEncoder urlEncoder) throws IOException
private final MacOutputStream macOutputStream;

public ClientDataSinkImpl(URLEncoder urlEncoder, Key hmacKey) throws IOException
{
this.urlEncoder = urlEncoder;

base64OutputStream = new Base64OutputStream();
macOutputStream = MacOutputStream.streamFor(hmacKey);

final BufferedOutputStream pipeline = new BufferedOutputStream(new GZIPOutputStream(base64OutputStream));
final BufferedOutputStream pipeline = new BufferedOutputStream(new GZIPOutputStream(
new TeeOutputStream(macOutputStream, base64OutputStream)));

OutputStream guard = new OutputStream()
{
Expand Down Expand Up @@ -92,14 +100,13 @@ public String getClientData()
try
{
objectOutputStream.close();
}
catch (IOException ex)
} catch (IOException ex)
{
// Ignore.
}
}

return base64OutputStream.toBase64();
return macOutputStream.getResult() + ":" + base64OutputStream.toBase64();
}

public String getEncodedClientData()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2012 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.apache.tapestry5.internal.util;

import org.apache.commons.codec.binary.Base64;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;

import javax.crypto.Mac;
import java.io.IOException;
import java.io.OutputStream;
import java.security.Key;

/**
* An output stream that wraps around a {@link Mac} (message authentication code algorithm). This is currently
* used for symmetric (private) keys, but in theory could be used with assymetric (public/private) keys.
*
* @since 5.3.6
*/
public class MacOutputStream extends OutputStream
{
private final Mac mac;

public static MacOutputStream streamFor(Key key) throws IOException
{
try
{
Mac mac = Mac.getInstance(key.getAlgorithm());
mac.init(key);

return new MacOutputStream(mac);
} catch (Exception ex)
{
throw new IOException("Unable to create MacOutputStream: " + InternalUtils.toMessage(ex), ex);
}
}

public MacOutputStream(Mac mac)
{
assert mac != null;

this.mac = mac;
}

/**
* Should only be invoked once, immediately after this stream is closed; it generates the final MAC result, and
* returns it as a Base64 encoded string.
*
* @return Base64 encoded MAC result
*/
public String getResult()
{
byte[] result = mac.doFinal();

return new String(Base64.encodeBase64(result));
}

@Override
public void write(int b) throws IOException
{
mac.update((byte) b);
}

@Override
public void write(byte[] b) throws IOException
{
mac.update(b);
}

@Override
public void write(byte[] b, int off, int len) throws IOException
{
mac.update(b, off, len);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2012 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.apache.tapestry5.internal.util;

import java.io.IOException;
import java.io.OutputStream;

/**
* An output stream that acts like a "tee", copying all provided bytes to two output streams. This is used, for example,
* to accumulate a hash of content even as it is being written.
*
* @since 5.3.5
*/
public class TeeOutputStream extends OutputStream
{
private final OutputStream left, right;

public TeeOutputStream(OutputStream left, OutputStream right)
{
assert left != null;
assert right != null;

this.left = left;
this.right = right;
}

@Override
public void write(int b) throws IOException
{
left.write(b);
right.write(b);
}

@Override
public void write(byte[] b) throws IOException
{
left.write(b);
right.write(b);
}

@Override
public void write(byte[] b, int off, int len) throws IOException
{
left.write(b, off, len);
right.write(b, off, len);
}

@Override
public void flush() throws IOException
{
left.flush();
right.flush();
}

@Override
public void close() throws IOException
{
left.close();
right.close();
}
}

0 comments on commit 95846b1

Please sign in to comment.